Snappaste (1) — BSidesTLV 2020 (450 pts.)

1byte
5 min readJun 30, 2020

--

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_filenamevariable 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_filenamevariable’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.

--

--

1byte

Just a person who likes hacking things. Feel free to tweet at me @guysudai1 and this is my team: https://ctftime.org/team/27128