I participated with Sec.SE CTF team at Meepwn 2018 CTF. I focussed mainly on the baby pwn challenge. Although I couldn’t get the flag during the competition. I think I was close enough to write a blogpost about this.
The challenge consisted of a Flask based webapplication. The application had the following endpoints:
/source: for downloading the python app.py source code.
/exploit: to deliver a payload via a POST request in order to exploit the service.
/: a simple interface to interact with all above endpoints.
The exploit endpoint is obviously of interest:
This endpoint accepts a base64 encoded payload, performs a certain test on the payload with test_i386(UC_MODE_32, payload), and if all goes well, passes it to a service listening on localhost:9999.
The test consists of a sandbox written with the Unicorn engine. An engine that emulates CPU instructions. The sandbox hooks into all interrupts, if 0x80 is encountered, then it will check the EAX register for blacklisted syscalls. If that’s the case, the ISBADSYSCALL session variable is set to True which means that the payload won’t be sent to the vulnerable process listening on localhost.
At the top of the Flask application, an interesting line app.secret_key = open('private/secret.txt').read() hints that we should read a secret.txt file in order to get the flag. Opening the binary ELF file with radare2, we get an idea what the binary does:
In short, it mmaps a region in memory with (PROT_READ | PROT_WRITE | PROT_EXEC) attributes, payload is read from stdin, mprotect is called to remove the PROT_WRITE attribute and finally the payload gets executed using call eax.
0x02 The attack plan
My basic and naïve approach is as follows:
Send a base64 encoded shellcode over HTTP.
The shellcode should read a file in order to extract the flag.
Since the payload is passed to the process without returning its response over HTTP, we need to communicate back the results via another medium. I chose sockets over TCP to send this data back to my server.
The shellcode should not use any of the blacklisted syscalls in order to bypass the Unicorn sandbox.
Avoid null bytes in the resulting shellcode as it might be interpreted as end of string during the attack.
Setup a remote server that listens on a port to receive the response from the shellcode.
0x03 The attack
To read the content of a file, we need two syscalls: open and read. These two syscalls are not allowed. I found this handy syscall list and scrolled through it. The following two syscalls were of interest openat and readv although they need to be used in a certain way. This took some time to figure out and especially debug. This in combination with socket functions made the assembly code relatively lengthy and challenging for this newbie who’s used to write only helloworld assembly programs. Luckily there was enough space for the shellcode so I didn’t have to optimize the length of the shellcode too much.
The secret file was however not in /home/babysandbox/private/secret.txt. I guessed this path since the binary was residing in /home/babysandbox/babysandbox. This basically meant I had to write some shellcode to traverse directories a la ls. This was not as straightforward as it required some fiddling with the getdents syscall. This is also where I failed to write proper assembly code as the output was quite giberish as can be seen in the following screenshot:
During the competition, I’ve missed the flag file residing at /flag. I could however find the secret.txt file which unfortunately didn’t contain the flag. The secret file was related to the Flask app itself which was used for crypto purposes. After the competition has ended, I’ve improved the two assembly programs which can be found at readdir and readfile in order to list a directory and extract the flag properly. The resulting shellcode is null-free:
Although I couldn’t extract the flag during the competition, I think the challenge offered some great learning experience. For one, writing the assembly code, diving into linux manuals and debugging the program was worth it. This however kind of made me “stubborn” so to say to suggestions. Gilles pointed out the possibility of using sysenter since the sandbox only checked for int 0x80. He even provided a link with a fully working example. I didn’t look into this solution as I already spent too much time on my own approach and felt that I was close enough. It turns out that I still had to write the directory listing program from scratch.
Another approach was to use execveat. I didn’t opt for this solution because I was afraid that the remote server wouldn’t support it. According to the documentation:
execveat() was added to Linux in kernel 3.19. GNU C library support is pending.
The execveat() system call is Linux-specific.
To make things worse, even my own Vagrant pwnbox setup didn’t support it. I quickly dropped this idea.
After the CTF ended, it is always nice to read other writeups and see how other people solved the challenge. This particular writeup shows how the stack setup is different in the sandbox environment (Unicorn emulator) compared to when its executed outside of it. A simple if statement would allow us to detect whether we’re inside a sandbox and exit if that’s the case. This means that all the conventional syscalls can be used outside of the sandbox. Another approach was to switch to x64 mode and use syscall instead.