📝 Sever-Side Template Injection (SSTI)

Explicación acerca de Server-Side Template Injection (SSTI)

Copyright © 2010-2025 Freepik Company S.L.

📋 Índice de Contenido

📋 Índice

💡¿Qué es un SSTI?

Un Server-Side Template Injection (SSTI) es una vulnerabilidad que ocurre cuando una aplicación web permite que la entrada de un usuario se inserte directamente en una plantilla procesada en el servidor sin ser correctamente sanitizada.

Dado que los motores de plantillas no solo sustituyen valores, sino que también pueden interpretar expresiones y ejecutar lógica, un atacante puede aprovechar esta situación para inyectar su propio código dentro de la plantilla.

Para comprender mejor este concepto, es importante explicar qué es una plantilla y qué es un motor de plantillas.


📄¿Qué es una plantilla?

Una plantilla es un archivo que define la estructura base de un documento (por ejemplo, HTML) e incluye espacios reservados o marcadores donde se insertan datos dinámicos.

Su función principal es separar la lógica del programa de la presentación. En lugar de generar manualmente todo el contenido con código de backend, el desarrollador define el diseño en la plantilla y deja “huecos” que se rellenan automáticamente con información en tiempo de ejecución.

Esto permite reutilizar el mismo diseño para mostrar datos distintos, manteniendo un código más limpio, organizado y fácil de mantener.


⚙️¿Qué es un motor de plantillas?

Un motor de plantillas es la herramienta o componente de un framework que se encarga de procesar las plantillas y transformarlas en un resultado final, normalmente en formato HTML, que posteriormente se envía al navegador del usuario.
Una vez que hemos comprendido estos tres conceptos, podemos pasar a la explicación de cómo se explota esta vulnerabilidad.


Al igual que en otras secciones de este blog, utilizaremos los laboratorios de PortSwigger para llevar a cabo los ejemplos prácticos.


🧪Laboratorios

En caso de que queráis ver la resolución directa de algún laboratorio, podéis utilizar el siguiente índice:

  • Lab 1: Basic server-side template injection
  • Lab 2: Basic server-side template injection (code context)
  • Lab 3: Server-side template injection using documentation
  • Lab 4: Server-side template injection in an unknown language with a documented exploit
  • Lab 5: Server-side template injection with information disclosure via user-supplied objects
  • Lab 6: Server-side template injection in a sandboxed environment
  • Lab 7: Server-side template injection with a custom exploit

Dado que los ataques de SSTI no se clasifican estrictamente en grupos, sino más bien en diferentes formas de enfoque, abordaremos la resolución de los laboratorios de manera progresiva.

Además, debido al tipo de ataque que es, nos apoyaremos en gran medida en la página de PayloadsAllTheThings, de donde obtendremos las líneas de código necesarias para la explotación.

IMPORTANTE:

Dado que este blog está escrito utilizando un motor de plantillas, los ejemplos de payloads mostrados no se visualizarán como texto literal en la web, sino que se interpretan por el motor.

Por este motivo, se recomienda no tomar los payloads de forma literal y consultar la página de PayloadsAllTheThings para obtener los ejemplos correctos.

En muchos casos he tenido que omitir ciertos caracteres como los corchetes, ya que, de lo contrario, el motor de plantillas interpretaría el contenido en lugar de mostrarlo tal cual.


Lab 1

SSTI básica

Para resolver este laboratorio, al igual que en la mayoría de los de esta sección, se nos solicita borrar un archivo ubicado en la ruta /home/carlos/morale.txt.

En este laboratorio se indica que el motor de plantillas es ERB, que pertenece al lenguaje Ruby.

Al interactuar con la página, se observa que en el primer post, al hacer clic, no se abre ningún contenido sino que aparece un mensaje dentro del HTML indicando que el producto no está disponible. Al revisar la URL, notamos que ese mismo mensaje se encuentra dentro de un parámetro:

https://0ac6007e03e439a680a5212c00b00063.web-security-academy.net/?message=Unfortunately this product is out of stock

Si modificamos ese mensaje con cualquier otro texto y recargamos la página, el contenido que hemos insertado se interpreta y aparece dentro de la estructura HTML. Esto sugiere que se está procesando directamente el input del usuario.

Consultando PayloadsAllTheThings y buscando payloads para ERB de Ruby, se observa que la inyección básica puede probarse con <%= 7 * 7 %>.

Si colocamos esto en el parámetro message de la siguiente manera:

https://0ac6007e03e439a680a5212c00b00063.web-security-academy.net/?message=<%= 7 * 7 %>

y recargamos, veremos que en el HTML aparece 49, confirmando que el input se está interpretando.

A partir de aquí, podemos llevar a cabo un RCE (Remote Code Execution) para borrar el archivo deseado utilizando el siguiente payload:

<%=(rm /home/carlos/morale.txt)%>

Lab 2

SSTI básica con error

Para resolver este laboratorio, nuevamente se nos solicita borrar un archivo ubicado en la ruta /home/carlos/morale.txt.

Además, se nos proporcionan las credenciales del usuario: wiener:peter.

Si iniciamos sesión con este usuario y accedemos a su perfil, observamos lo siguiente:

Al desplegar el campo de nombre preferido, se observan múltiples opciones.

Esto significa que, al publicar un post, el nombre que se mostrará a los demás usuarios será el valor que hayamos seleccionado en este campo.

Si interceptamos una petición al cambiar el nombre preferido, se observa que se tramita de la siguiente manera:

Si, por ejemplo, probamos en el parámetro blog-post-author-display algo como #$%^, estamos saliendo del contexto original de la plantilla, ya que la mayoría del código que se interpreta en las plantillas se encuentra entre llaves. Con estos caracteres especiales, cerramos el contexto anterior y abrimos uno nuevo.

Al visitar un post donde hayamos publicado un comentario, se genera un error y, por el mensaje de salida, podemos identificar que el motor de plantillas utilizado es Tornado de Python.

Consultando PayloadsAllTheThings y buscando payloads para Tornado, el payload básico de prueba es 7*7.

Si introducimos esto en el parámetro blog-post-author-display y visitamos un post con nuestro comentario, veremos que nuestro nombre se reemplaza por 49, confirmando que el input se interpreta correctamente.

A partir de aquí, podemos realizar un RCE (Remote Code Execution) utilizando el siguiente payload para borrar el archivo /home/carlos/morale.txt:

%import os% os.system('rm /home/carlos/morale.txt')

Lab 3

SSTI usando documentación

Para resolver este laboratorio, nuevamente se nos solicita borrar un archivo ubicado en la ruta /home/carlos/morale.txt.

En este laboratorio, en lugar de recibir las credenciales habituales, se nos proporcionan las de un usuario denominado content manager content-manager:C0nt3ntM4n4g3r.

Al iniciar sesión con estas credenciales y acceder a un post, observamos que tenemos la capacidad de editarlo.

Dentro del editor, aparecen partes del texto que utilizan plantillas, por ejemplo ${product.stock}.

Si reemplazamos ese valor por caracteres aleatorios para forzar un error y aplicamos los cambios, se genera un mensaje de error que revela cuál es el motor de plantillas utilizado.

En este caso se trata de Java Freemarker.

Por lo tanto, como en los laboratorios anteriores, acudimos a PayloadsAllTheThings para buscar el payload correspondiente que nos permita realizar un RCE (Remote Code Execution).


Lab 4

SSTI en un motor de plantillas poco común

Para resolver este laboratorio, nuevamente se nos solicita borrar un archivo ubicado en la ruta /home/carlos/morale.txt y se nos vuelven a dar las credenciales content-manager:C0nt3ntM4n4g3r.

De manera similar al laboratorio anterior, al editar un post y forzar un error, identificamos que esta vez el motor de plantillas utilizado es Handlebars.

Una vez más, consultamos PayloadsAllTheThings para localizar el payload de RCE (Remote Code Execution) correspondiente.


Lab 5

SSTI con filtrado de información

En este laboratorio, en lugar de solicitarnos borrar el archivo /home/carlos/morale.txt, el objetivo es obtener la secret key de la aplicación.

Al igual que en los dos laboratorios anteriores, se nos proporcionan las credenciales content-manager:C0nt3ntM4n4g3r.

Tras forzar un error en la edición de un post, identificamos que el motor de plantillas utilizado es Django (Python).

En este caso, al consultar PayloadsAllTheThings, no encontramos directamente un payload para listar la clave privada. Sin embargo, investigando un poco más en internet, descubrimos que es posible obtenerla mediante settings.SECRET_KEY.


Lab 6

SSTI con para romper el sandbox

Para resolver este laboratorio, se nos solicita leer un archivo ubicado en la ruta /home/carlos/my_password.txt.

Una vez más, se nos proporcionan las credenciales content-manager:C0nt3ntM4n4g3r.

Tras forzar un error en la edición de un post, identificamos que el motor de plantillas utilizado es Freemarker (Java).

El payload de RCE que permite leer el archivo es el siguiente ${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/carlos/my_password.txt').toURL().openStream().readAllBytes()?join(" ")}

Este payload devuelve el contenido del archivo en formato decimal 97 117 99 113 51 104 101 115 122 48 54 97 110 119 104 119 51 52 98 115.

Debemos convertir esta secuencia de valores a ASCII, obteniendo así la contraseña aucq3hesz06anwhw34bs


Lab 7

Para resolver este laboratorio, se nos proporcionan las credenciales de wiener:peter y se nos pide que borremos el archivo ubicado en la ruta /home/carlos/.ssh/id_rsa.

Una vez más, tenemos disponible la opción de cambiar la forma en la que queremos que se muestre nuestro nombre cuando comentamos en un post. Si interceptamos la petición en BurpSuite y probamos con la inyección básica 7*7, vemos que aparece 49 al lado de nuestro nombre. Si forzamos un error poniendo caracteres especiales, nos damos cuenta de que el motor de plantillas utilizado es Twig de PHP. No obstante, si probamos todas las opciones de RCE listadas en PayloadsAllTheThings, ninguna funciona. Por ello, debemos buscar otro enfoque para cumplir el objetivo.

En esta ocasión, también se nos otorga la posibilidad de subir un avatar para mostrar al lado de nuestro nombre. Si subimos una imagen e interceptamos la petición, podemos experimentar con ella:

  • Si cambiamos el contenido por test, el sistema devuelve un error en un archivo llamado User.php.
  • Si eliminamos el content-type, aparece el error User->setAvatar('/tmp/avatar.jpg', '')

Este error filtra una función interna del servidor encargada de cambiar el avatar. Además, muestra cómo es el paso de parámetros esperado: primero la ruta y después el tipo de archivo. En el ejemplo, aparece vacío porque eliminamos el content-type.

Cuando cambiamos el nombre que se muestra en un comentario, la petición tiene el formato user.Funcion(). Si modificamos esa petición y establecemos blog-post-author-display=user.setAvatar('/home/carlos/User.php', 'image/jpg') y la enviamos, al abrir la foto en una nueva pestaña podemos ver el código fuente del archivo.

Con curl podemos traer el archivo completo a consola:

curl -s -X GET 'https://0acb00a3043125f580aac72f00ba0096.web-security-academy.net/avatar?avatar=wiener'

En su interior, observamos la siguiente función:

public function gdprDelete() {
    $this->rm(readlink($this->avatarLink));
    $this->rm($this->avatarLink);
    $this->delete();
}

Esta función elimina el avatar que esté en uso actualmente.

Con esta información ya es posible resolver el laboratorio. Lo que debemos hacer es:

  • Cargar como avatar la clave id_rsa que queremos eliminar.
  • Llamar a la función gdprDelete() para que el servidor la borre.

Primera petición:

blog-post-author-display=user.setAvatar('/home/carlos/.ssh/id_rsa', 'image/jpg')&csrf=j1skDWV8Vi03uTHAiRFk0JqZUCMGwJMJ

Segunda petición:

blog-post-author-display=user.gdprDelete()&csrf=j1skDWV8Vi03uTHAiRFk0JqZUCMGwJMJ

Y con todo esto, somos capaces de resolver el laboratorio.


Copyright © 2025 Mario Ramos. Distribuido bajo Licencia MIT.