We felt like there were not enough pasting services, so we created Snappaste! For better privacy, each pasted text can only be accessed once. We care about performance, and therefore we developed Snappaste in C++. We hope that we didn't introduce any security bugs :)URL: https://snappaste.ctf.bsidestlv.com/To compile, Use the following commands:gcc -c -std=c99 zlib/*.c
g++ -c -std=c++14 snappaste.cc
g++ -o snappaste *.o -pthread
TL;DR at the bottom for whoever just wants to read the solution (a script is attached)
In this challenge, we are given a link to a paste website which allows its users to create a single-use paste with a randomly generated 16 byte name. Like the following:
Okay, so after viewing the site’s features we can safely dive into the source code which was also given by the challenge. We extract the zip to our challenge folder, and compile the challenge as the challenge creator instructed to do (notice we added the g
flag in order to enable debugging symbols which will allow us for an easier time debugging):
gcc -g -c -std=c99 zlib/*.c
g++ -g -c -std=c++14 snappaste.cc
g++ -g -o snappaste *.o -pthread
While inspecting the code, I came across several interesting paths to solve this challenge:
- The
backdoor_filename
variable is pretty suspicious, so it might have to do something with it. - Might be some sort of path traversal vulnerability (although we can disqualify this because this is a PWN challenge)
Delving deeper into the source code we can see there are a few preconfigured paths we can use (I’ve only listed the interesting ones):
- /paste (POST) — Alows us to paste in content
- /view/[a-z]{16} (GET)— Tries to read a paste with the provided name, and if it exists it’s also deleted.
- /backdoor/[a-z]{16} (GET)— Was supposedly added for testing purposes, and provides us with the
backdoor_filename
variable’s address.
Note: [a-z]{16} is basically a regular expression which matches 16 (no more and no less) characters from a to z, so abcdefghabcdefgh will match, but abcdefghabcdefg (15 characters), Abcdefghabcdefgh (1 uppercase letter) will both not match
The view
endpoint also has an extra feature, if the backdoor_filename
is the same as our provided filename we get the flag, and achieve great victory.
Unfortunately for us, the line that modifies the backdoor_filename
variable with our provided content has been commented out and we have to find a different way to modify the backdoor_filename
variable.
The next interesting thing we probably should investigate is the paste function since it is the main functionality of our program, and we will probably find the error in it.
In order to understand better what’s going on, I have drawn a small graph to demonstrate the process happening.
The sizes at the top are a part of the PASTE_NETWORK_HEADER
struct which is defined as the following:
struct PASTE_NETWORK_HEADER {dword metadata_size;dword data_compressed_size;dword data_decompressed_size;};
Let’s go over line by line what the paste function is actually doing:
The first if
verifies we have the sizes described at the top, the second if checks if we have more data than we actually claim to have. After that if, we might get a bit confused since what does metadata + header->metadata_size
have to do with anything? Well if we remember metadata is a pointer, it actually makes sense. Let’s look back at the earlier graph:
So now this makes sense, raw is a pointer to the beginning of the request, the sizes are described by the PASTE_NETWORK_HEADER
struct so raw + sizeof(PASTE_NETWORK_HEADER)
will be metadata, and therefore the compressed data will be at raw + sizeof(PASTE_NETWORK_HEADER) + metadata_size
which makes sense.
Next, we allocate a block of memory the size of the metadata + the size of the decompressed data + the size of the customly defined struct. So it will look like the following in memory:
next we uncompress the given data (which was compressed with zlib’s compression) into the paste_received->data
variable. (Which lies in the second part of our graph)
After the decompression, we copy the metadata into the paste_received->metadata
pointer, which lies in the first part of our graph.
Since we are given the backdoor_filename
‘s address we might think of ideas to override paste_received->metadata
pointer with the backdoor_filename
‘s address therefore copying our metadata to the variable. One way to do this would be to actually to declare we have less decompressed data than we actually have, and then when decompressing the data it will actually overflow into the paste_received struct. But the uncompress
, unfortunately for us, checks for this, and will return with an error.
After a long time of searching, I tried to mess with the provided sizes, and noticed something very interesting
dword num_bytes = header->metadata_size + header->data_decompressed_size + sizeof(PASTE_RECEIVED);
The num_bytes variable is a DWORD, but it is getting assigned to a DWORD + DWORD + sizeof(PASTE_RECEIVED), this is a classical overflow, and we can actually cause the num_bytes to be smaller than the actual data we provide. Now remember that we need to copy over 16 characters from the metadata to the backdoor_filename
variable, so we’ll set the metadata size to 17, and set the decompressed data size to 0xffffffff
which is the maximum variable size we can have.
From here, we have a write-what-where
exploit, by overriding the paste_received->metadata
pointer with whatever we want, we can write the metadata variable into the backdoor_filename
variable. So this is the final exploit:
TL;DR: In this challenge we have a DWORD overflow, because the website allocates the decompressed data before the PASTE_RECEIVED struct, we can override the metadata pointer with our data therefore allowing a write-what-where
situation and modifying the backdoor_filename
variable with our own paste link.