Datos del reto
| Campo | Valor |
|---|---|
| CTF | Nullcon CTF 2026 |
| Reto | Web2Doc2 |
| Categoría | Web |
| Flag | ENO{weasy_pr1nt_can_h4v3_f1l3s_1n_PDF_att4chments!} |
Descripción del reto
El servicio convertía una URL en PDF usando Flask y WeasyPrint. A diferencia de la versión anterior, ya no existía un endpoint cómodo para leer la flag, así que había que conseguir que el propio generador de PDF leyera /flag.txt por nosotros.
Reconocimiento
La primera idea obvia era pasar file:///flag.txt como URL principal, pero eso no funcionaba porque el backend descargaba la página antes de enviársela a WeasyPrint. Sin embargo, revisando cómo trabaja WeasyPrint apareció una opción interesante: los adjuntos en HTML.
Se podían declarar así:
<link rel="attachment" href="URL" title="flag">
Si WeasyPrint resolvía ese href, el contenido terminaba metido dentro del PDF final.
Análisis
La clave estaba en separar dos contextos:
- el backend sí necesitaba una URL pública para descargar el HTML;
- pero una vez descargado ese HTML, WeasyPrint resolvía recursos adicionales desde el propio servidor.
Eso permitía alojar una página pública e incluir dentro de ella un adjunto con file:///flag.txt. Aunque la URL principal tuviera que ser HTTP, el archivo adjunto se resolvía localmente durante la generación del PDF.
Resolución
La página maliciosa podía ser tan simple como:
<link rel="attachment" href="file:///flag.txt" title="flag">
El flujo fue este:
- Subir ese HTML a una URL pública.
- Enviar esa URL al servicio de conversión.
- Dejar que WeasyPrint generara el PDF.
- Descargar el PDF resultante.
- Extraer el adjunto o revisar el stream hasta encontrar la cadena
ENO{...}.
En el PDF final, el contenido de /flag.txt quedaba incrustado como adjunto, así que se podía sacar sin necesidad de leer el archivo directamente desde la web.
Flag
ENO{weasy_pr1nt_can_h4v3_f1l3s_1n_PDF_att4chments!}