Web2Doc2
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 había un endpoint directo para leer la flag, así que había que conseguir que el propio generador de PDF leyera /flag.txt.
Reconocimiento
Pasar file:///flag.txt como URL principal no funcionaba porque el backend descargaba la página antes de enviársela a WeasyPrint. Revisando cómo trabaja WeasyPrint aparecía una opción que servía para esto: 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 incrustado dentro del PDF final.
Análisis
La clave aquí 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.
No había que hacer que el backend abriera file:///flag.txt como página principal. Era suficiente con hacer que WeasyPrint lo tratara como un adjunto durante la fase de renderizado.
Resolución
La página maliciosa podía ser tan simple como esta.
<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!}