Our company developed a unique Linux binary called “cat”.
We recently discovered that our competitors from the Antivirus company VirusExpress are blocking our cat
binary. It is now signed as a virus. Word is that you are a badass security researcher. We need you to infilitrate their server and empty their database.
Download cat
kileak score: 0xffff/0xfff
You won!
You badass researcher. To collect your T19 challenge coin, please send the flags in an email to ctf@twistlock.com
Good job!
Set the same cookie (t19userid) for the target and attack: Virus.Express
Stumbled over the T19 challenge from Twistlock last week and really enjoyed it, so I decided to do a writeup for the trip through the “official” challenges and also for getting the hidden flag.
Challenge 1: Get started with the json api and get access to the remote server
We were given a user cookie and the virus express page as our target.
Setting the cookie as described in the challenge, gets us to the virus express web interface.
We can upload a file there, for example the provided cat file, which will be shown as a virus. Though, not much to do with this functionality for now, but the source of the page points us to the next step.
So it seems there’s a backend service, with which we can communicate through /api. Since I didn’t want to bother too much with that api through a browser, I set up a short python script for this, also being more flexible on later steps.
Armed with this, we can try to explore the api and find out more about its usage, since no info about it was available at that point.
Ok, so it expects application/json data.
Let’s update the session header accordingly
Trying it again will now result in an error page, since the application wasn’t able to parse our input, giving us some additional information.
From line 52 we can work out, how the input json has to be built up:
Updating the script to send proper json payloads to the service:
With this, we’ll get through the parsing, but end up with another error, this time in the is_virus function.
Ok, the hash length has to be divisible by 4, and if no cmd is specified, /home/ben/dbclient will be executed, which probably compares the hashes to the internal “database”, but what’s more interesting is line 8:
Easy command injection :)
So, if we define a valid hash, we can execute any command as the user, which is running the script on serverside.
Ok, that wasn’t too bad, and got us the first flag. After dropping it on the challenge page, the next assignment is to leak the dbclient.
Challenge 2 : Leaking dbclient and get access to user ben
Since we now have kind of a shell to the system, we won’t have to mess around with web stuff anymore. Leaking dbclient is pretty forward, just cat it, base64 encode it and decode it again locally.
Finally a binary to work with, things start to get interesting. Note, that the dbclient runs as setuid and belongs to the user ben. So exploiting it, will give us access to the ben user.
But first, let’s see how the binary works, and what we could do with it.
Though I reversed the complete binary at this point already, understanding how the protocol works and the client communicates with the server isn’t needed for stage 2, so let’s ignore it for now.
There’s an obvious buffer overflow in handling the arguments, which can be used to overwrite the hashfuncptr. Since the binary uses a bit-and for “assigning” the hash_func, we’re able to forge the hashfuncptr to call an almost arbitrary address in the binary instead (which conveniently will get our input as parameter).
Seeing that the binary contains __libc_system.plt, this is an easy target to get code execution. We can overflow dest to overwrite hashfuncptr, which should then get anded, which results in pointing to __libc_system.plt.
Only one small obstacle left to overcome:
This means our complete payload must consist only of characters from HASH_CHARSET. Thus we cannot just and the address of __libc_system.plt but have to find some other chars from this charset to result in the same value.
Another issue is, that we are not allowed to use any whitespaces, which constraints us in the commands, we’ll be able to execute with system later on. I got around this by preparing a script in /tmp with the command execution from stage 1 and then just execute this script via dbclient (which will then be run as ben, since it’s setuid).
For overwriting hashfunc_ptr, we can just use 0x4141414141432a68 consisting only of valid chars (and will result in 0x400a60).
Ok, second flag done, on to the next assignment. This time, they ask us to “delete” the database of virus express.
Challenge 3 : Leak server binary and exploit it to delete the database
First, we’ll need to leak the server binary. Fortunately ben has a copy from it, though it’s only readable by him.
Well, we already have code execution as user ben, so we can do something similar to stage2, just this time, we’ll be reading it with our second exploit through dbclient.
While at it, I also leaked libc from the remote system to get a better idea of the setup (this can be done with the stage 1 exploit, since it’s readable by everyone).
From stage 2 I already had a good idea of the protocol used in communication between client and server, which is now needed for stage 3.
We’ll be building our own client to have more control over the packages, which will be sent to the server.
Let’s start with the package structure itself. For this we can take a look at how the client sends a request package to the server.
From the respective offsets in the buffer, the package structure can be derived to something like this:
Won’t go through the complete reversing of the client and server communication now, but to sum it up
The server creates a socket secretSock, on which he waits for connections
The client connects to the socket secretSock and sends a request package
Action 1: Ping
Action 2: Check if entry (from payload) exists in server database
Action 3: Read entry from index (specified in payload)
Action 4: Check if db is empty (byte at addr == 0x0 ) and answer with flag, if it’s empty
With this, we can start writing our own client for communicating with the server.
Back to reversing the server binary. When it receives a package from the client with a hash, it will mmap the database file into memory and check, if it can find the hash from the received package in it:
If the database isn’t already mapped, it will open the file /opt/db/base and maps its content to a memory region. After that, it will copy our package content into a buffer and then search through the mapped area, comparing the found hashes with our payload.
But by copying the package content into a buffer of only 32 bytes size, we have another overflow, which allows us to overwrite for one the offset, at which it looks for the hash to compare and also the tmp_buffer pointer, into which it will copy the hash.
Since the offset isn’t checked for correct range, we can use this to read outside of the mapped region. And by overwriting tmp_buffer, we can write that value to an arbitrary address. Though, since strcpy is used, no null bytes are allowed in our payload, so we cannot use completely arbitrary values for overwriting offset (or we’ll lose the possibility to overwrite tmp_buffer).
Since I asummed aslr would be active on the server, we’d need some leaks before being able to do something useful with this (which turned out as a wrong assumption later on, but well…).
Let’s see what action 3 will do:
Again, no range check, which means we can also specify out of bounds value here.
Whilst it might not be too obvious: if this is the first request to the server, addr won’t be initialized (thus it’s 0x0), so we can do direct reads with it, without knowing the address of the mapped region (which would be a problem with aslr).
I used that to leak bss, so I’d have at least some libc addresses at hand.
Since the database must not be initialized to use the direct leak, crash_service can be used to restart the service to make sure addr is not initialized.
Testing it locally gave:
For testing on the server, I adjusted the exploit script, so it will upload our compiled exploit to the server (similar to leaking in the first stage, just the other way round now) and execute it on the server.
Wait, what? There’s no aslr active on the server…
Ok, this should make it easier to exploit it via check_entry. Though the mapped region on the remote server was off by 0x1000 to my local one, I found it at 0x00007ffff7ff3000 after some more leaking. With this knowledge, we can calculate the proper offsets to have an somewhat write-what-where.
Since the mapped region address contained a null byte, it wasn’t usable in the payload, since strcpy would just stop at that null byte (at least, that’s what I was thinking at that point).
So, my first approach was to overwrite addr itself to point to some null pointer instead of the database string, but that turned out quite difficult, since
would align that strcpy. Since addr was located at 0x603108 we would need an 8 byte string followed by a valid address (since we could only copy to 0x603100).
While wasting some hours on this approach (which should pay off for the next stage though), it struck me, that exactly this & 0xF0 which harrassed me the whole time, can be used to write to the mapped region. Since it sets the LSB to 0, we can just pass an offset there and it will be set back to the start of the mapped area (facepalm moment…).
This means, we’d just have to find some address containing a null byte and copy it to the mapped region. Since strcpy terminates the copy with a null byte, it would just overwrite the byte at addr with 0x0.
Piece of cake compared to the initial approach.
The final flag, we arrived at the end? At that point, it got quite late and I had to get some sleep.
Just to notice the next morning, vakzz has reached 0xffff. Props to vakzz for digging deeper :)
Challenge 4: Get root rce and find the hidden flag
From my first approach, it was presumably possible to gain rce, if one could get a real arbitrary write and not just reusing available data.
It took some time to find all pieces together, but let’s start with check_entry again. The only possibility to get an arbitrary write would be, if we could somehow use our package content as a source for the strcpy.
I tried to use a negative offset to wrap around memory space to access the values from my payload, but it turned out, that it wasn’t possible to land in the range of the stack (at least it didn’t work out for me).
So, back to overwriting addr, maybe get a better starting point, from which we could work towards the stack.
From my initial approach on stage 3 I remembered, that I stumbled over an 8 byte string there, followed by a stack address. But the application segfaulted, when using it, since memcmp found a string at that address and thus check_entry continued walking over the stack until it went out of the mapped area, at which point I rejected it earlier.
I decided to give it another try
Validated that this address also looked similar remote (it was just a bit off due to different stack layout).
The application crashed again, since memcmp returned a value != 0… So, why not overwrite memcmp with something, that wouldn’t do that. After some searching for good reference addresses, I found the got table in /lib/x86_64-linux-gnu/libz.so.1.2.8 at 0x7ffff7dd8000.
This address is in a good range for a negative offset from the mapped db region, and it contains a pointer to malloc.plt.
Passing a stack address as a parameter will result in malloc failing to allocate memory and always just return 0. Perfect…
Now, we can safely copy the address from vdso_getcpu to 0x603100, which will overwrite the addr ptr with a stack pointer. And since memcmp will now return 0, it will exit check_entry properly, leaving us with a new base address.
addr will now point to the stack address stored at vsdo_getcpu. Also leaked it before overwriting addr and then updated db_addr accordingly afterwards to calculate with the correct new base address from then on.
We can now place an arbitrary value in our payload, which won’t be copied by strcpy, but will be available on the stack (in our source package, the server received). Since the new base address now points behind our package payload on the stack, we can easily address values from it with a negative offset.
With this we now have an arbitrary write-what-where now and I already wanted to start writing a ropchain, when I realized we’re already at the finish line. We can just overwrite memcmp again, but this time with system, resulting in rce.
Since memcmp will be called in check_entry with our payload as a parameter and the application will crash again after that, because system will return a non null value, we also have to provide the command to execute directly in that payload.
Also, we won’t be able to retrieve the output from our command, since the application will crash before sending any response package. But we can get around this by just piping the output into /tmp and then read it via the stage1 exploit.
Since the server is run by root all our commands will also be executed in root context, so we have a meta-root-shell now:
And there it is, the hidden flag flag{!!!Well_this_is_was_honestly_not_expeted_grats!!!}, which gave us 0xffff.