Server Side Template Injection (SSTI)
What is a template engine?
A template engine separates the logic layer from the presentation layer in an application. We can insert variables or complex logic directly into templates, which then get rendered into the final page.
Popular template engines: Jinja2 (Python), Twig (PHP), ERB (Ruby), FreeMarker (Java), Handlebars (JS).
If user input reaches the template engine unsanitized and uses valid template syntax, we can potentially achieve template injection. Note: client-side template injection also exists - same testing approach, but code executes in the browser instead of on the server.
Methodology
- What is the application doing? What buttons can I click? What can the user do? What links are there?
- Where can I send input? Possible injection points?
- Can the input be reflected back? Check: stored output, comments, source code, etc.
- Check for HTML injection first (
<h1>hi</h1>). SSTI usually also has XSS. - Run the generic detection payload to see if we get an ERROR 500.
- Fuzz with a template wordlist. Compare response lengths for anomalies.
- Identify the template engine from errors or behavior differences.
- Look up engine-specific RCE payloads.
- PoC: Run
idorwhoami.
Detection
Generic detection payload
{{$<%=(*`|.'#-%>;}}
If this returns an ERROR 500 or a stack trace, the application is likely processing template syntax.
Fuzzing with ffuf
- Start Burp Suite Intercept Mode
- Input text, i.e. FUZZ
- Save it to a file → SSTI0x01.req
- Change parameters → FUZZ (if not used in the payload)
- Run ffuf:
ffuf -request SSTI0x01.req --request-proto http -w /usr/share/seclists/Fuzzing/ssti.fuzz -v -c
- Observe the extreme ends (high/low response sizes)
- Observe the errors it generated:
Fatal error: Uncaught Twig\Error # <- this tells us the engine is Twig
Error messages often reveal the engine directly.
Confirming server-side vs client-side
After injecting {{7*7}}, check the page source (Ctrl+U). If the source shows 49, it’s server-side (executed on server, result returned). If the source shows {{7*7}}, it’s client-side (executed in browser).
Important: always check the response in Burp Suite too. In the course challenge, {{7*7}} appeared unchanged in the browser because client-side JavaScript was updating the page. But the raw response in Burp showed 49, confirming SSTI. Don’t trust what the browser renders - always verify in Burp.
Identifying the Template Engine
Different engines handle the same input differently. Use these to fingerprint which one you’re dealing with:
Jinja2 (Python/Flask)
{{ '10' * 3 }}→101010(string repetition){{ 10 * 3 }}→30{{ config }}→ shows Flask config- RCE:
{{ request.application.__globals__.__builtins__.__import__('subprocess').Popen('whoami', shell=True, stdout=-1).communicate() }} - RCE:
{{ request.application.__globals__.__builtins__.__import__('os').popen('whoami').read() }}
Twig (PHP)
{{ '10' * 3 }}→30(casts string to int){{ 10 * 3 }}→30{{ dump(_SERVER) }}→ server configuration- RCE:
{{['id']|filter('system')}}
ERB (Ruby)
<%= '10' * 3 %>→101010(string repetition)<%= 10 * 3 %>→30<%= ENV.inspect %>→ displays underlying server environment
The key difference: Jinja2 and ERB repeat strings ('10' * 3 = 101010), while Twig casts to int ('10' * 3 = 30). This is how you tell them apart.
Exploitation
Once you know the engine, look up payloads:
- HackTricks: https://hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection/index.html#twig-php
- PayloadsAllTheThings: https://swisskyrepo.github.io/PayloadsAllTheThings/Server%20Side%20Template%20Injection/
- Wordlist: https://raw.githubusercontent.com/swisskyrepo/PayloadsAllTheThings/refs/heads/master/Server%20Side%20Template%20Injection/Intruder/ssti.fuzz
Test payloads step by step. Do not despair if one fails - just try others. Each engine has different syntax and different paths to RCE.
Lab Notes
Lab (SSTI 0x01)
Input step by step:
Hello # Normal
<h1>Hi</h1> # HTML injection
<script>prompt(1)</prompt> # XSS
Then fuzzed and found Twig error. Tested {{7*7}} which returned 49. Then used {{['id']|filter('system')}} for RCE.
Challenge
There were input fields to create a card. After using the error payload I saw it was using Twig again. I used {{7*7}} which should return 49. But after executing it, it displayed the payload as-is. However, intercepting the request in Burp Suite and checking the response showed the correct result. The reason is that client-side JavaScript was updating the card display and overwriting the server response.
Lesson: always capture and review responses in Burp Suite. The browser can lie to you.
Testing Checklist
- Identify all input fields where user data is reflected back on screen - comment fields, profile names, card generators, message templates, etc.
- Test for HTML injection first (
<h1>hi</h1>) - SSTI targets usually also have XSS, which confirms unsanitized reflection - Send the generic SSTI detection payload
{{$<%=(*|.’#-%>;}}` - if you get an ERROR 500 or stack trace, the app is processing template syntax - Fuzz with a dedicated SSTI wordlist using ffuf:
ffuf -request req.txt --request-proto http -w /usr/share/seclists/Fuzzing/ssti.fuzz -v -c - Check error messages carefully - they often reveal the template engine directly (e.g.,
Twig\Error,jinja2.exceptions,ERBtraces) - Identify the template engine using behavioral differences:
{{ '10' * 3 }}returns101010in Jinja2/ERB (string repetition) but30in Twig (integer cast) - Confirm server-side vs client-side: check the page source (Ctrl+U) - if it shows
49for{{7*7}}, it is server-side. If it shows{{7*7}}literally, it is client-side - Always verify in Burp Suite - client-side JavaScript can overwrite the server response in the browser, hiding successful injection. The raw response in Burp shows the truth
- Once the engine is identified, look up RCE payloads on HackTricks or PayloadsAllTheThings - test them step by step
- PoC with a safe command first:
idorwhoami- confirm code execution before escalating - Include in report: the template engine identified, the detection payload and its response, the RCE payload and output, and whether it is server-side or client-side execution