The service was a statically linked 32-bit C binary. The service consisted of the binary itself, a script (
to start it, a folder (
data) to store the service data and a Docker environment.
One thing to note here is that all these files were mounted into the Docker as read/write
The binary had very few protections enabled and had multiple RWX-segments.
Upon connecting to the service, you were prompted to either register a new user or login
with an existing one.
After the login, you could either append a new receipt to an index or print the receipt stored at
When the connection is closed, all added receipts were stored on disk in a folder named after the user.
The filenames were decimal numbers representing the index of the receipt with the filecontent being the receipt itself.
Exploit 1: Missing Bound Check on Appending
The stored receipts were not only stored on disk but they also existed in memory in an array (
struct receipt_element. The array had place for 64 receipts.
Upon appending a new receipt at index
x, it was also added to the array at index
Even though, the index was checked to be smaller or equal to 63, it was never checked whether
the index is greater than 0.
This allowed an out-of-bounds write.
To better understand the write-primitive we have to take a look at binary.
This means that we can create a pointer and let it point to user controllable heap data.
As the heap was mapped as RWX, we first tried overwriting the
Unfortunately, we could not overwrite these hooks due to
struct receipt_element being
12 bytes which means that we can only overwrite pointers correctly aligned to these 12 bytes.
Just as we were searching for other potential targets the team The Duck started exploiting the service.
We took a look at their exploit and saw that they were exploiting the same bug and using the offset
The offset -212 was pointing to
__libc_IO_vtables and overwrites a pointer which was triggered
when exiting the binary.
Hence, we used the same offset.
Exploit 2: Path-traversal; Out-Of-Bounds Access; Buffer Overflow
We thought most teams will likely fix the previous exploit by adding the missing bound check
or checking if a user tries to
append a file starting with a minus.
Hence, we chained a few other bugs together to build a second exploit.
When trying to register a user, the service checks whether there exists a file named
password in a
folder, named after the user, to determine whether a user already exists.
The register function looked roughly like this:
There are two overflows in there. In both calls to
sprintf in both cases writes
to a buffer that is 128 bytes in size, but the content exceeds this length as the username itself
can be 128 bytes and the strings ‘data/’ and ‘/password’ are added.
If we now register a username with a name that is 128 chars long, we first overflow into the canary
in the first call to
sprintf and afterwards overflow from
passwd_file with 5 bytes (length of ‘/data’).
This results in a call to the second
fopen with the last 5 bytes of our username as filename.
Hence, we can abuse this to create a file with a name of our choice aslong as the filename is atmost 5 bytes.
Additionally, we can control the first 128 bytes of this file.
Also note that this file will be created in the service directory root as there is no
data/ prefix after the overflow.
As there are no further checks for characters contained in our username we can specify
../ as a username
to create a user in the service root directory. When we chain these two vulnerabilites together we
can create a user in the service root directory and abuse the
sprintf overflows to create a file named
which contains shellcode like in the previous exploit.
All that is left now is to find a way to trigger this shellcode again. Luckily, there is one.
After a user logs in, the binary iterates over all files in the user directory, tries interpret the
filename as a number and loads their content to the offset in the
This looks roughly like this:
This means that all what we have to do triggering this functionality by logging in and we can
once again access out-of-bounds of
receipt_arr to trigger our shellcode.
We first create a file named ‘-212’ in the service root folder, then we register a user named
and finally login as that user to trigger our shellcode.
The files were inside the Docker were mounted writable and we wanted to make our RCE more persistent.
One idea that came to our mind was that we could just forward the traffic of other teams to our own and already patched
This way we could prevent other teams from further stealing flags and the gameserver would just submit
their flags directly to our server, hence we could just grab and submit them.
For that, we overwrote the service start script (
run.sh) and later also the service binary itself
with a script that acts as a telnet client to our own service instance:
As the service had a quite simple logic, we decided to just rewrite the service ourselves in Python. This turned out to be really successful, as we lost 6% SLA points but not one single flag.
Our exploits worked really well but we had one big snafu.
As our second exploit requires control over the username
../ this was a one-shot exploit.
Unfortunately, we recognized this after throwing the exploit around.
If you look closely, you can see that our second exploit generates random passwords for the
and never stores them.
The result was that we locked ourselves out of all
../ users that we created and could not use them
for further exploitation.
All in all, this was a really fun service with cool post-exploitation possibilities.