Skip to content

Instantly share code, notes, and snippets.

What would you like to do?

The Catch 2021

This started as quick notes rather than a full, detailed write-up. Please use this text with caution. If you notice any error in this document, please let me know.

The theme of the competition was investigating remains of a lost civilization: some IT equipment still running was found. A helicopter takes us to the excavation site so we can begin solving the challenges.

Table of contents

Solved challenges:

Training Camp


Data Extraction


Unsolved challenges:

Data Extraction

System Access

Solved challenges

Encrypted Archive

We are given two files:

  • lead_to_password.txt

The flag is in, stored in the encrypted ZIP archive, but a password is needed to decrypt it. If we did not have any other lead, we could start brute-forcing the ZIP password with an appropriate tool. Fortunately, lead_to_password.txt gives us a hexadecimal string 8fd2011515522f6879dddd55d18a83d7, suspiciously looking like a hash – the length corresponds to MD5, but the entropy is lower than expected from a randomly distributed cryptographically secure hash. (Note that MD5 is too weak for today's standards.) Rather than assuming it is really a valid MD5 hash and trying to brute-force it ourselves, we can try searching the web... This leads to one of numerous websites listing words/strings and their hashes, which get subsequently indexed by search engines. We find that md5("mytreasure") leads to our hash and mytreasure is indeed the correct password.

Cat Heads Language

We are given an ancient writing – a cryptic image with a cat head and a question mark on the top, followed by lines of triplets of cat head images next to a character:

(click for a larger version)

The letters seem to be from the "alphabet" used for flags (A-Z, {} and -, in other challenges also lowercase) so we assume just selecting the correct lines leads to the flag. There are a few types of the cat head images, one corresponds to the cat next to the question mark. Interpreting the "cat head + question mark" in the first line as "where is this cat?", we try finding the first few lines which contain that head ... we get FLAG{ so we continue, eventually getting the flag. As the flag is short, we proceed manually selecting the correct lines, otherwise we could automate finding the correct heads with e.g. OpenCV. Fortunately the flag consists of meaningful words, so we can detect possible mistakes like skipped letters before submitting the flag.


(Note: other flags do not seem to contain meaningful words, therefore they are omitted from the writeup.)

Unknown file

You get a file of an unknown format; the goal is to find the used format and somehow get the flag from it.

Looking at the file with a hex editor shows it may be a zip file ("PK" header), which could also be found with the file or binwalk tools. No extra data seem to be present after the end of zip file (not a polyglot). The zip file contains:

  • mimetype
  • Configurations2/popupmenu/
  • Configurations2/menubar/
  • Configurations2/progressbar/
  • Configurations2/toolbar/
  • Configurations2/statusbar/
  • Configurations2/images/Bitmaps/
  • Configurations2/floater/
  • Configurations2/toolpanel/
  • Configurations2/accelerator/
  • manifest.rdf
  • styles.xml
  • meta.xml
  • content.xml
  • Thumbnails/thumbnail.png
  • settings.xml
  • META-INF/manifest.xml

This appears like a "XML-in-zip"-type of document (something like docx/xslx/pptx/... or the odt/ods/...). The files mimetype and META-INF/manifest.xml (and possibly others) point to it being an "OpenDocument Spreadsheet" file. After changing the file extension ".ods", it can be opened in LibreOffice or Excel.


The document contains suspiciously-looking letters (from the correct flag alphabet), but seemingly no flag. Looking at the neighboring cells, the "empty" black cell at the bottom contains the following formula: =CONCAT(B3;E3;C4;E5;B7;C7;D7;B8;E8;B9;D9;B10;C10;C11;E11;C13;D13;B14;C14;C15;C16;E16;B17;C18;D18), concatenating the letters above. (This formula can be also found by looking at the content.xml file contents.) To see the cell's result, we just need to change the font color, revealing the flag.

Problematic Executables

You are given three files:

  • executable (a 64-bit ELF)
  • executable.exe (a Windows PE file)
  • executable_core.part (a snippet of the main function's C source code):

int main(int argc, char *argv[]) {
  int num;

  if (argc != 4){
    printf("Alarm! Bad usage! Alarm!\n");
    return 1;
  if (strcmp(argv[1], "show-me-the-secret") != 0 || strcmp(argv[2], "please") != 0){
    printf("Alarm! Bad usage! Alarm!\n");
    return 1;
  num = atoi(argv[3]);
  if (num < 4 || num > 7){
    printf("Alarm! Bad usage! Alarm!\n");
    return 1;

  return 0;


Using Ghidra, we can check if both executables match the provided snippet of the main function – the two different binaries seem to be provided as a convenience for people trying to solve the challenge on Windows or Linux. (On a Mac, the Linux binary can be run inside a Docker container, or the Windows binary via Wine – possibly natively, or maybe even in a Linux environment inside Docker...)


The program expects 3 arguments: two strings "show-me-the-secret" and "please", and an integer. The integer needs to be between 4 and 7 and is passed to a print_secret function, which writes the output character by character, subtracting the integer from a locally-stored sequence of bytes (starting with LGRM).


We could compute the offset (knowing the flag starts with FLAG{), but since there are only 4 possible values, we can just try running the program repeatedly, increasing the integer until we succeed the get the flag:

./executable show-me-the-secret please 6

The Services

We are given an IP address of a server. The goal is to examine the services running on it.

After a few unsuccessful manual attempts to connect to common ports, I resorted to nmap, which discovers there are five services listening on TCP ports (at least on the default port numbers it scans).

  • 58080: an HTTP server - sending a GET / HTTP/1.0 request e.g. using netcat, returns Server: FLAG1{RkxBR3tZcm} as one of the response headers
  • 4445: SMB/Samba – the workgroup name is FLAG2{M3LV}
  • 2022: an SSH server – connecting to the port sends the header SSH-2.0-FLAG3{czcV} ... the sub-flag is hidden in the server version string
  • 2021: appears to be an FTP server – when connecting we get 220-FLAG4{YtRk1rMi00OXlXfQ==}
  • 1720: this one is irrelevant to the challenge - probably used only for monitoring purposes (nagios)

The service types were discovered by trial and error, using last digits of the port number as a hint.

Let's put it all together. The sub-flags appear to be base64-encoded, but some are not valid base64 strings on their own (bad length). Concatenating the sub-flags and then decoding it as one base64 string gives us the flag.

Note: nmap can also try to detect the running services (e.g. server versions) when supplied with correct switches (e.g. nmap -sV -O This could save some work.

Domain Name System

We are given an IP address of a DNS server and are supposed to find as much information as possible.

Sometimes interesting things like subdomains can be found in TXT or SRV entries, or ANY requests, possibly even in whois, by brute-force enumeration, or in certificate transparency logs. A hint tells us the server is standalone so we can start digging around on the server (pun intended).

With dig @ -x we get the PTR/reverse record: the server's name is hamster.super.tcc or ns1.super.tcc. The server appears to be a name server for the super.tcc domain This time the task is simple – the server allows zone transfers so we can just ask it to give us all the data with dig @ AXFR super.tcc.


This reveals a suspicious TXT record for squirrel.super.tcc. Decoding that gives us the flag.

The Motivation

We once again get an IP address to have a look at. Apparently the server was "somehow used to increase the motivation and morale of people".

A hint tells us to look at low port numbers. We discover TCP port 17 ("Quote of the Day" service) responds to our connection, sending us a motivational quote.

(Let us now dedicate a minute of silence to how simple, efficient and un-bloated the services were in the early networks, before "the whole Internet" turned into a large blob of JavaScript served over HTTP(S). This 1-minute countdown timer might also serve as an example of an unnecessarily complex webapp. Once the bloated webapps become self-aware and take over, and our civilization disappears, the future archeologists will not stand a chance understanding even the most basic "modern" apps. OK, enough with the exaggerated rant; let's get back to solving the challenge.)

One of the quotes in circulation says:

+ Once a correct sequence of connection attempts is received, the firewall rules are dynamically modified to allow the host. Sequence of three ports is 65000 + {DNS, LDAP, Syslog). Btw. look at 65000 again.  - The Catcher

The quote tells us to use port knocking to open a service at port 65000. After looking at /etc/services for the default ports for the mentioned services, we can start knocking:

nc -v 65053
nc -v 65389
nc -v 65514

This should open the port for us (likely for a limited amount of time) and we can connect to it to get our flag (in just a raw TCP response):

nc -v 65000

The Dark Side

We are given a domain name thecatchu6jlyqgen3ox74kjcfr5lmwdc7jqj3vmekq6y45dmvo5xmad.onion to check out.

The .onion TLD is used in the Tor network. After trying an apparently non-functional online WWW -> Tor gateway, I incorrectly assumed the service on the domain is not a web server on the default port. So I resorted to installing the software in a virtual machine – just apt install tor – so I could try other services/ports on the server. Running tor opens a local SOCKS proxy server which can be used to connect to the hidden services in the network. (Note: I read if anonymity – the aim of that network – was needed, using a Tor Browser bundle is recommended instead of a manual setup, to prevent leaking information elsewhere.)

Visiting the domain in a web browser (or via curl) did show a static page: a ~1.1MB large string of base64-encoded text. Decoding revealed a string: one character, a separator ;, and another base64 string. Trying a few more recursive levels of decoding the string after the separator, the pre-separator characters appear to form a meaningful sentence. Making a hacky Bash script simplified the decoding:

s="$(base64 -D)"
echo "$s" | cut -d';' -f1 >&2
echo "$s" | cut -d';' -f2

Calling cat the_downloaded_webpage.txt | | | | | ...| prints the decoded characters to stderr, which reveals the flag.

The Geography

We are given a URL to a webpage saying:

Challenge task : Try to visit again from UY, Uruguay
Challenge timeout (sec) : 120

When the countdown reaches zero, we have to start from scratch. The website recognizes/remembers us by a session cookie.

Unfortunately, due to physics constraints, we can't travel to arbitrary countries this fast. So we have to use the next best thing – pretending to the server that we did, by connecting from elsewhere – relaying the request over a public proxy server in the requested country. There are lists like, where we can filter the servers by country.

The only remaining step is to quickly find a working proxy server in given country, switch to it and send a request (or reload the challenge webpage) a few times. Since we have 2 minutes, we can afford to do that manually (e.g. changing the proxy server address in Firefox), or using curl, we just change the command line.

curl --cookie cookies.txt --cookie-jar cookies.txt --preproxy socks4://IP:port

Note that we need to preserve the session cookie between requests: we need to both save the session cookie, and use it in the subsequent requests (therefore the command contains --cookie and --cookie-jar).

Sometimes finding a server in the requested country is not possible (at least in your favorite proxy list) – fortunately we can start from scratch and hope for a better sequence of countries.

After visiting the URL from three(?) countries, we get the flag.

Private Network

We are given an IP address that should give us access to a private network, and a note there is a web server somewhere in the range.

The server runs a Squid HTTP proxy server. The range contains 2048 addresses so it is feasible to scan (it takes about a minute). Once again, a Bash script (+ curl) can be used:

for last in {0..255}; do
    for ip_pre in 10.20.3{2..9}; do
    curl --proxy $ip > data/$ip.txt &

Looking for outliers in the file size reveals a file that is NOT an error message from Squid, like others. The file corresponding to IP contains the flag, meaning we found the correct webserver.

I also tried noobing around with nmap, but didn't manage to convince it to use an HTTP proxy. Maybe this would be an easier solution for someone who knows it better.

Unknown Server

We get an IP address, which seems to respond to a PING (ICMP echo request) in a strange way. A packet capture reveals the response contains a string "Send ICMP type 8 code 0 and Epoch time in payload".

This looks easy, right? The BSD-based ping command in macOS supports the -p command to specify the pattern to send in the packet so we just put the timestamp there...


While the packet generated this way does contain the epoch time in the payload (I tried hex or ASCII-encoded and others), it also contains other data like a precise timestamp when the packet was generated, which the server does not accept. Looking at the packets in Wireshark is useful for debugging (and reading the response), but it did not alert me to the problem as "Data" in the packet parser showed the desired data and I assumed "Data" == payload. Trying a different ping implementation did point me in the right direction.

Manually crafting the packet in Python (via scapy) and sending it via RAW socket was the way. It required some fiddling with the contents (e.g. the server wants an ASCII-encoded timestamp).

sudo python3 -c "from scapy.all import *ł import time; ts = int(time.time()); pkt = Ether(dst='router_mac_address', src='my_iface_mac_address') / IP(src='my_ip_address', dst='') / ICMP(type='echo-request', code=0) / str(ts); sendp(pkt, iface='en0')"

The server then replied with the flag – visible as text in Wireshark.

Docker Image

We are given an exportedsaved Docker image: a tar archive tcc_docker_42.tar containing some metadata and multiple tar archives (one per layer) containing changes of the container's filesystem between layers.

Running the container tagged tcc/python3test:docker (with all layers applied, by docker load <tcc_docker_42.tar and corresponding docker run --rm -it tcc/python3test:docker sh) and browsing it for possible suspicious programs/data does not reveal anything obviously strange, so we dive deeper (as suggested in a hint) into lower layers for previous versions of the filesystem.

The tar archive contains a description file manifest.json where we find the list of layers and a "Config" file 415d3d922eaaf2a95cc7a0ff1777d0e9096db9853bc78ae9655224d5279aad45.json. If "history" in that file can be believed, the layers were created by rather harmless commands:

  • setting the default command (CMD command in Dockerfile)
  • installing python3, vim, mc and screen
  • running echo "Why python3? Python2 ought be enough for everybody!" (RUN in Dockerfile) and potentially unsafe commands:
  • using an unknown base image (FROM command in Dockerfile)
  • adding unknown files (ADD or COPY in Dockerfile)

The last point appears the most interesting: a file /usr/sbin/init gets changed a three times. Let's look at that. One of the earlier layers adds/updates the file with the following content (boring parts were omitted):

	# generate image
	var = ['651', '1L0', '5D0', '6D0', '301', '2A0', '3G0', '2r2', '4{0', '7K1', '1f2', '4}2', '9-1', '8L0', '8x1', '1K1', '0s2', '0m1', '0F0', '271', '5P1', '7m0', '4-1', '9-0', '3c2']
	tim ="RGB", (1024, 512), (0, 0, 0))
	font = ImageFont.truetype('cour.ttf', 32)
	draw = ImageDraw.Draw(tim)
	for v in var:
		draw.text((int(v[2])*300 + int(v[0])*30 + 3, 255), v[1], (255, 255, 255), font=font)

This is just an obfuscated way to draw some text character by character. Looking at the documentation for the text-drawing function we see the first and third character in the var array items make up the x coordinate (y is constant = 255), the middle character is the actual printed/drawn character (note the characters are in the correct "alphabet" used by flags). Let's simply print the characters with their x coordinate:

var = ['651', '1L0', '5D0', '6D0', '301', '2A0', '3G0', '2r2', '4{0', '7K1', '1f2', '4}2', '9-1', '8L0', '8x1', '1K1', '0s2', '0m1', '0F0', '271', '5P1', '7m0', '4-1', '9-0', '3c2']
for v in var:
	print('{} {}'.format(
		int(v[2]) * 300 + int(v[0]) * 30 + 3, # x coordinate
		v[1]                                  # character

This, with sorting by the coordinate and some cleanup gives us the hidden text (flag): python | sort -n | cut -d ' ' -f 2 | tr -d '\n'

Alternatively, assuming the original script works and has all dependencies installed (and the font), we could instead try removing the last few layers from the image, pack it as a tar file, ~~re-import/~~re-load the image into Docker and run it there to generate the image with the flag. I have not tested that.

Note that there are two different sets of Docker commands for saving and loading Docker images or containers to/from tar archives. save/load saves/loads all the layers and metadata of an image while export/import saves/loads the container's filesystem to "flat" archive, discarding any layers. Usually the former is what you want to use.

Nomen Omen

We are given a Windows binary executable (supposed malware), which creates a copy of itself using a new name when it is run, and the file content also changes. The goal is to analyze how the name changes.

Using Ghidra, we find the "main" part of the program in .text.startup: doing some sanity checks (like length of the filename) copying the current executable into %APPDATA% and replacing the contents of the copy at the offset 0x7e28 with current timestamp...


We can also make more observations using static analysis:

  • The new filename (generated using the next function) is based only on the previous filename (getstring extracts the filename without path or extension from the full path).
  • Ghidra's decompiler does not like the next function – only prints an error message "Unable to find unique hash for varnode". Maybe this could be overcome by giving Ghidra more information about datatypes? or other functions? or check if things like calling conventions were selected/autodetected correctly? Falling back to reading assembly would be an option, but the function was too long for that – I would not trust my noob-level skills and rather short attention span... Using a different disassembler did help, but the pseudocode looked nontrivial (with SIMD operations), which discouraged me from proceeding this way.
  • The change in file contents seems to be just the timestamp update.
  • There is some extra stuff like replacing | with - and back.

We can also get an overview of the program's behavior by running it in wine in strace.

A hint for the challenge says "The number of different names is very probably limited." Therefore we could try applying the next function repeatedly. E.g. we could use a debugger, break at exiting the function, log the output, copy the output to where the function expects the input, and call the function again. Automating this would give us a sequence of file names. Alternatively we could try calling the next function from the outside (e.g. from a Python script or C# with P/Invoke?). Unfortunately debugging or interop in Windows is not my strength nor passion :-|

In the end, a quick and dirty bash script did the trick (repeat: find the only .exe in Wine's appdata directory, run it with wine, move away the old .exe and print/list the new name):

while true; do
    name="$(echo *.exe)"
    if [ $(echo "$name" | wc -l) -ne 1 ]; then
        exit 1
    wine64 $name >/dev/null
    mv -- "$name" old/
    ls -- *.exe

After 724 (+-1) iterations we got a filename in the correct format: flag{fwsg-iboz-hmlt-pqhz}.exe. Surprisingly it ran rather fast, taking just a few minutes, if I remember correctly.

Unsolved challenges

I did not solve the following challenges – some due to exhausting ideas how to proceed, others due to lack of time. But I hope documenting my attempts may be useful for someone.

Blogging Web Site

We get a URL with a page containing links to two posts (blog entries). We are supposed to get content of all entries. shows the content of the first entry – just a dummy document

The second entry at hints at existence of private posts and mentions "mongo". Unsecured "NoSQL" stores like MongoDB are sometimes discovered, leading to a data breach, but I did not find it running on the server (at least on the default ports, I don't remember if I tried scanning for others).

A hint said to "get information about used technologies" so the next step was exploring the webapp to gather more info:

<b>Fatal error</b>
:  Uncaught Slim\Exception\HttpNotFoundException: Not found. in /opt/ctfb1/web/vendor/slim/slim/Slim/Middleware/RoutingMiddleware.php:91
Stack trace:
#0 /opt/ctfb1/web/vendor/slim/slim/Slim/Routing/RouteRunner.php(72): Slim\Middleware\RoutingMiddleware-&gt;performRouting(Object(Slim\Psr7\Request))
#1 /opt/ctfb1/web/vendor/slim/twig-view/src/TwigMiddleware.php(125): Slim\Routing\RouteRunner-&gt;handle(Object(Slim\Psr7\Request))
#2 /opt/ctfb1/web/vendor/slim/slim/Slim/MiddlewareDispatcher.php(147): Slim\Views\TwigMiddleware-&gt;process(Object(Slim\Psr7\Request), Object(Slim\Routing\RouteRunner))
#3 /opt/ctfb1/web/vendor/slim/slim/Slim/MiddlewareDispatcher.php(81): class@anonymous-&gt;handle(Object(Slim\Psr7\Request))
#4 /opt/ctfb1/web/vendor/slim/slim/Slim/App.php(215): Slim\MiddlewareDispatcher-&gt;handle(Object(Slim\Psr7\Request))
#5 /opt/ctfb1/web/vendor/slim/slim/Slim/App.php(199): Slim\App-&gt;handle(Object(Slim\Psr7\Request))
#6 /opt/ctfb1/web/public/index.php(35): Slim\App-&gt;run()
#7 {main}
  thrown in 
 on line 

This stack trace reveals Slim framework is used. There was an ancient vulnerable version - at least some line numbers match the current version, but e.g. Slim/Routing/RouteRunner.php does not exist in the vulnerable 2.5.0 yet. We can't use that. We are in /[...]/public/ while the framework is (relatively) in ../vendor.

  • Trying post titles like index, index.php, ../index etc. did not seem to give any new output. (I was thinking the posts could be stored in files and hoping for path traversal to the index.php file to reveal its source.)
  • Another malformed query like (or anything containing a dot) shows a "Not Found" error from PHP's built-in server – it is not recommended for production use, but I don't know if it is vulnerable in any useful way.
    • Only GET requests seem to be allowed by the request router (or the index.php file) and all non-file-download requests seem to be handled by the router.
  • Changing the title parameter from string to an array like[]=a yields an error/notice
Notice: Array to string conversion in /opt/ctfb1/web/vendor/twig/twig/src/Environment.php(358) : eval()'d codeon line 51
  • I could not think of a way to inject any code into the eval, even when looking at the source code of the framework.
  • Other injections I could think of also did not work – probably the input is well-sanitized.

This is all I could think of. I think I missed something more or less obvious as this is a challenge for just 3 points.

Social Network

We are presented with a website with a login form and are supposed to "Get access to the system and export any valuable data". This means getting logged into an admin account.

The website sets a suspicious session cookie like session=eyJ1c2VybmFtZSI6bnVsbH0.YWtRMw.BZJpMbses0Ykq_XlAQ_hbrJX5nE. The session key consists of three parts separated by .:

  • eyJ1c2VybmFtZSI6bnVsbH0 ... this is "URL-safe base64"-encoded (without training "=") JSON: {"username":null}. Let's call it payload.
  • YWtRMw ... a (base64-encoded) value, monotonically increasing every second (a timestamp)
  • BZJpMbses0Ykq_XlAQ_hbrJX5nE ... probably a hash or HMAC or a signature:
    • whenever the timestamp changes, this also changes (if we make two requests in the same second, it stays the same)
    • if we re-send the cookie without modification, it is "silently accepted" (we are not asked to save a new cookie by "Set-Cookie:" response header), but if we change any of the first two components or the hash itself, we get a new session cookie (same payload as above, just with current timestamp and updated hash)

The possible solution lies in changing the username in payload to something like "admin" or "administrator", but we would need to re-compute the hash. This means understanding its format is necessary to proceed.

Asking the server to generate several session cookies (by repeatedly sending a request without a cookie) shows the length of the hash after decoding is consistently 20 bytes with high entropy, pointing to it likely being some cryptographic hash function. SHA-1 would fit the length. After a few manual attempts at concatenation of the first two components with different encodings and separator, I made an abomination of a bash script with 6 levels of nested for loops trying different hash functions, prefixes, splitters and encodings of the first two encodings to brute-force the hash. Unfortunately without any success. The hash could as well be a HMAC or a salted hash with a key/salt long enough to make it secure against this attack.

A hint for the challenge says "It will be useful to find and download the application" – this would indeed be very useful to discover how the hash is constructed, but I could not think of a working way to download the application. So I put the challenge to rest, waiting for any useful idea to come.

(I later noticed that when submitting invalid credentials, the decoded payload JSON also contains "_falshes":[{" t":["message","invalid credentials"]}],. If this is the source of the error message, this might be useful for determining if a modified payload is accepted. But without a way to generate a new hash, matching known values, it does not make much sense to start with modifications.)

File Share

We are given a URL, which redirects to with the following content:

Current path is \\localhost\shared\message.txt
Hey you, try to get Samba password for user 'shared', then search flag.txt  :-)

Changing the values of server= or file= does not seem to change the output (I was hoping for some kind of path traversal). Only changing the name of, or removing the server parameter yields Apache's(?) Internal server error; the file parameter does not seem to be used. As noted in the challenge's hints, there is Samba running on the same server. We know the username, but do not know the password. I tried several common passwords, but none worked.

As I ran out of ideas how to collect any additional information/hint about the password, the last resort would be brute-forcing the Samba password. Is that allowed? Is this the intended solution?

Phone Book

We are given a URL of a phone book web app, from which we are supposed to extract phone numbers (visible only to logged-in users).

The phone book app has a login form and a search function, which can be used to enumerate the users. It requires at least 2 characters so using a Bash for loop and curl/wget seems like a suitable tool for the job:


for n in {d..z}{a..z}; do

curl '' \
-X 'POST' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Host:' \
-H 'Referer:' \
--data 'query='$n > data/$n.txt


Manually filtering the files (e.g. sorting by filesize and filtering out the replies without any matched user) is sufficient. (Using grep | sort | uniq etc. would be better if there were more users.) Entering the few found users to the login form (with a dummy password) gives us "Client not found in Kerberos database" error, and in one case (user tytso) "Decrypt integrity check failed" – the user seems to exist.

As pointed to by the challenge hints (and an error message), there is Kerberos running on the server, which we can use for "Kerberoasting". We can use one of the scripts in Impacket (impacket-GetNPUsers -dc-ip SUPERPHONEBOOK.TCC/tytso --no-pass) to request some data that is encrypted by the user's password from the server (no pre-authentication is needed) – we know the format of the data, so we can try brute-forcing the password using John the Ripper. (Hashcat does not support the hash type $krb5asrep$17$ or $krb5asrep$18$ ... the server provides these two – we can change the priority in the GetNPUsers script – but the server does not offer the MD4-based one.)

Unfortunately my wordlist did not contain the password, nor a wordlist created by pydictor --sedb or pydictor -plug scratch, providing it with publicly known information of Theodore Ts'o or related websites. And brute-force attack (even with an extended character set) did not find any result in reasonable amount of time. Maybe I was too impatient or missed something important.

Torso of Web Server

We have a URL to a directory listing with

  • flag.txt (inaccessible without authentication),
  • an Apache config file (restricting access to flag.txt),
  • a C source file ticketer_mit.c
  • and apache2.keytab. From what I read, keytabs should NOT be accessible as they can be used for authenticating with servers (not just for verification of authentication, like used here). I managed to authenticate with the Kerberos server using the keytab:
kinit -V -k -t apache2.keytab HTTP/ctfb4.tcc@SUPERCLIENT.TCC

For this to work I had to fake the DNS queries to SUPERCLIENT.TCC with locally running dnsmasq:


and set the resolver's address in /etc/resolv.conf. (Later, also an A record of ctfb4.tcc had to be added.) Note that just overriding the /etc/hosts is not enough as kinit requests a SRV record.

klist should now show we have a ticket.

But when requesting the flag.txt, the authentication token (sent in Authorization: Negotiate [some long base64 string] header) was not accepted – I was still asked to authenticate. I tried curl and Firefox to reduce the chance of me not understanding how to use the SW.

I suspect the ticket would need to be somehow changed (likely by the provided ticketer_mit.c tool). In its source I saw a reference to "Silver ticket" (re-use of a ticket for a different service than originally intended), and two unused #define statements, maybe related to the other service... I feel I was quite close to the solution. Unfortunately I had to abandon this challenge due to lack of time.

General remarks

I enjoyed the competition, but for the first time did not manage to finish all the challenges. The (subjective) increase in difficulty is a good thing, otherwise the CTF would get boring for us, regular participants. The main problem is that I should have reserved more time to solving the challenges. I would appreciate more information in the FAQ regarding what "interactions" with the competition servers are allowed: is port-scanning allowed (it is sometimes considered an attack)? Is brute-forcing login credentials an intended part of any challenge's solution (e.g. the "File Share" challenge)?

While not staying in the top 20/30 spots on the leaderboard, I learned something new about Kerberos and its pitfalls, tools like impacket and pydictor and got general knowledge about what potential issues to look for when setting up or securing systems. And most importantly I learned to decode cat hieroglyphs :3

I found it useful to do a screen capture while working on the challenges – it helped me a lot when writing this writeup, and I saw where I (unnecessarily) spent too much time. (Unfortunately I can't publish the video as it contains possibly private info like messages in notifications etc. and editing it would be too time-consuming.) I will consider using a separate account for the next CTF to solve this issue, if anyone is interested.

I'm looking forward to reading others' writeups!

Last but not least: greetings to atx and Jakluk, and greetings and congratulations to JaGoTu!

-- rendm (or @rendm2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment