Saltar al contenido
cd ..
HTBEXP-0003
RESUELTA

Spookifier

Web Fácil 4 min de lectura
SSTIMakoRCE
ZEZenthikjul 2026

Writeup en video

Resolución completa del challenge Spookifier, paso a paso.

Writeup del challenge Spookifier (HackTheBox · Web · Muy fácil). Es una web que "espeluzna" el texto que le mandes: agarra tu frase y la devuelve escrita en varias tipografías raras. El problema es que, para armar esa respuesta, mete tu texto dentro de una plantilla del lado del servidor sin sanitizarlo → SSTI (Server-Side Template Injection), y de ahí saltamos a RCE para leer la flag.

Fase 1 · Primer vistazo

Al abrir la web hay un único campo de texto. Escribimos cualquier cosa (por ejemplo test) y la app nos devuelve ese mismo texto renderizado en cuatro fuentes distintas. No hay login, ni más rutas: todo pasa en ese input.

Que el servidor tome nuestra entrada y la devuelva "decorada" es justo el patrón que conviene sondear: si la está incrustando en una plantilla, podríamos inyectar en ella.

Fase 2 · Confirmar el SSTI

La prueba clásica de template injection es mandar una operación matemática y ver si el servidor la evalúa en vez de mostrarla tal cual:

${7*7}

Si en la respuesta aparece un 49 en lugar de ${7*7}, el servidor está ejecutando lo que le pasamos. Y así es.

Detalle clave: de las cuatro fuentes, tres "filtran" o transforman caracteres como <, % y >, así que el payload se rompe. La cuarta fuente los deja pasar intactos — es la salida donde hay que fijarse para ver el resultado de la inyección.

La sintaxis ${...} es la del motor de plantillas Mako (Python). Lo confirmamos con un payload que solo Mako resuelve de esta forma:

${"z".join("ab")}

Si devuelve azb, estamos ante Mako. Ya sabemos qué motor tenemos y podemos apuntar a sus objetos internos.

Fase 3 · De SSTI a RCE — leer la flag

Con Mako no hace falta nada rebuscado: podemos abrir archivos del servidor directamente desde la plantilla. La flag suele estar en la raíz como /flag.txt, así que la leemos:

${open('/flag.txt').read()}

La respuesta (en la cuarta fuente) nos escupe el contenido del archivo: ahí está la flag. 🚩

Si el reto tuviera restringido open, Mako igual nos deja llegar al sistema operativo a través de sus utilidades internas, por ejemplo ejecutando un comando:

${self.module.cache.util.os.system('cat /flag.txt')}

Pero para Spookifier con open('/flag.txt').read() alcanza.

Lecciones

  • Nunca construyas una plantilla concatenando input del usuario. El texto del usuario debe ser un dato que se pasa al render, no parte del código de la plantilla. En Mako eso significa renderizar una plantilla fija y pasar la entrada como variable, no interpolarla dentro del ${...}.
  • Escapá y validá la entrada antes de que toque el motor de plantillas. Filtrar <, %, > en algunas fuentes no alcanzó: quedó una salida sin filtrar y por ahí entró todo. Los controles hay que aplicarlos de forma consistente, no en algunas ramas sí y en otras no.
  • Aislá y da mínimos privilegios al proceso. Un SSTI en Mako da ejecución de código: si el proceso corre en un sandbox, sin acceso al filesystem sensible y como usuario sin permisos, el impacto de la RCE se reduce muchísimo.
  • Preferí motores logic-less (tipo Jinja2 con autoescape, o plantillas que no evalúan expresiones arbitrarias) cuando el contenido depende de la entrada del usuario.