HITB Lockdown CTF 2020 - Receipts
26 April 2020 by alfink and dwe
Service Overview
The service was a statically linked 32-bit C binary. The service consisted of the binary itself, a script (run.sh
)
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
an index.
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 (receipt_arr
)
consisting of 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 x
.
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 __malloc_hook
and __free_hook
.
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
-212.
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 Script
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
. 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 buf
to 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 -212
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 receipt_arr
.
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.
Exploit Script
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.
Persistence
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
service.
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:
Patching
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.
Conclusion
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 ../
user
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.