Skip to main content
Volver a writeups

Writeup técnico

Web2Doc2 – Nullcon CTF 2026

---

nullcon CTF 2026

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:

  1. Subir ese HTML a una URL pública.
  2. Enviar esa URL al servicio de conversión.
  3. Dejar que WeasyPrint generara el PDF.
  4. Descargar el PDF resultante.
  5. 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!}