httpd (pwn)
httpd was a pwn challenge from FCSC 2022, of hard difficulty.
We were asked to exploit a sandboxed HTTP server given the binary and the sources.
TL;DR
Stack buffer overflow in HTTP header
Leak canary, PIE, libc and shared memory pointer
Write second stage payload to shared memory
Format string bug in parent process allows to rewrite its own memory
Rewrite seccomp filter to cancel out the sandbox
ret2libc in child process
Preliminary recon
Naively, we try accessing the web server directly from our browser, and are greeted with a login window.

Filling in admin:admin grants us authentication, but only displays the following message: Congratulations! Now get the flag.
Let's check out the files we're given.
Lots of protections, but there's even more to come.
The sources of the challenge consist of several files:
The main file, httpd.c, implements a fork server after mapping a shared memory page:
The request function forks the process and launches a sandbox in the child process, inside of which the HTTP requests will be processed.
The filter.bpf file catches our attention. It implements a BPF filter:
This filter is effectively enforced by the sandbox, thanks to seccomp:
This means that the child processes are only allowed to use a very select number of syscalls. With these syscalls, even if we manage to exploit a child process, we won't be able to get a shell or read a file. It also seems the BPF filter does not bear any specific weakness.
Auditing the sources: a first vulnerability
Next step is to audit the sources to identify potential vulnerabilities.
We quickly find a problem in worker.c. The following function, checkAuth, takes as input the base64 string that is sent through the Authorization HTTP header (e.g. "Authorization Basic YWRtaW46YWRtaW4=").
It appears the b64_decode function writes the decoded base64 buffer directly to creds without performing any length check for as long as there are bytes to decode. Also a determinant fact is that no null byte is appended to the decoded base64 string. This accounts for a stack-based buffer overflow in the checkAuth method.
We can trigger the bug by sending a request that looks like this. Note that each line feed should be preceded by a carriage return (\r).
Exploiting the buffer overflow
Leaking canary and PIE
We cannot directly overwrite RIP as there is a canary protection. However, remember that no null byte is appended to our overflowing byte array in the stack. This means we can brute-force the canary one byte at a time:
Every time the server does not crash and we are successfully logged in, means we found a new correct byte.
It is also important to keep the connection alive (Connection: keep-alive) as the parent process will, in this case, loop and fork again to handle the next request. This way, the canary will stay the same across the forked processes.
Once we determined the 8 bytes of the canary, next thing we can do is brute-force the return address in order to leak the PIE base. This works pretty much the same way, although we can speed up the search because we know the return address ends in 0x89e and its two most significant bytes are null.
Leaking libc base
Now that we can overwrite RIP and know the PIE base, we can use a few gadgets from the binary and proceed to leak libc base. This will give us useful gadgets and functions for the next steps.
The idea is simply to use ret2plt: we will use the puts entry in the PLT so that the server leaks a pointer for us. In this case, we arbitrarily decide to leak puts@got which points to libc. As we are given the libc version, we can then calculate its base.
Now... what should we do? We can run arbitrary ROP chains and have access to the whole libc, but we still can't do anything really promising because of the seccomp filter. Time to find something else.
Second vulnerability in the parent process
In order to do anything really interesting such as popping a shell, we would have to escape from the sandbox, for instance by leveraging the parent process.
How can we reach the parent process? Well, recall the main function:
There is a shared page of memory between the parent process and its children! Its structure is the following:
Then, this shared structure is used in the audit function, which serves for logging purposes.
At this point, I was tired and I incorrectly read the source, thinking there was yet another stack-based buffer overflow when the LOGIN %s string was copied to msg with our username (obviously there is not, since it stops at sizeof(msg)). Therefore, I spent a lot of time trying to manage to leak the shared memory pointer so that I could write an arbitrary second stage payload inside it, including null bytes.
Thanksfully, it didn't go to waste as getting such a primitive was still useful to exploit the actual vulnerability, which we will talk about now.
The vulnerability actually lies in the use of this function:
The syslog function takes a format string as input, and we control the msg buffer because shared->username is copied in it (if we are logged in, i.e. shared->loggedIn == 1). This also requires that the child process returned cleanly (no crash).
Leaking the shared memory pointer
Testing locally, I noticed the offset between the shared mmaped page and the libc base was constant. Therefore, I first finished my exploit by hardcoding this offset in my script.
Obviously, it turned out that this offset was completely wrong on the remote, certainly due to how the kernel manages memory differently.
I explored several methods to leak this pointer. In particular, I wasted a lot of time trying to leak it from the stack:
Leak stack pointer through
&environin libcCalculate pointer to shared memory pointer in the stack
Leak shared memory pointer
Again, I didn't manage to make it work on the remote as I wasn't able to locate the pointer in the stack (which was, of course, at a different offset than locally).
Especially, my exploit was taking a painfully stupid time to run and I had to wait dozens of minutes each time to brute-force the canary and PIE. Indeed, I had to set a ~500ms timeout for every byte (and even then this was not necessarily enough as I often stumbled upon false positives). Therefore, debugging my exploit on the remote was excruciating.

Next, I tried leaking the shared memory pointer directly with well-chosen gadgets. The shared->username pointer was in rdi at the end of the checkAuth function, so leaking rdi through a ropchain would be enough.
Again, I spent a lot of time trying to chain libc gadgets to move rdi to an interesting register (typically rsi, so that I can write to it through read, or leak it through some function like printf...), without success. I didn't immediately think of writing the register to memory (next time, I will know!). Eventually, I came up with this chain:
Writing a second stage payload to the shared memory
This part is rather straightforward: return to read@plt.
It is important to note that we want to exit the process cleanly to trigger the correct path in the audit function next time the parent process runs it. We use the _exit function defined in the binary that directly syscalls exit (the libc exit will not work with the seccomp filter).
After sending this payload, the server will ask for 0x300 bytes and we can overwrite the shared memory as we want.
Exploiting the format string bug
We now fully control shared->username. As we saw earlier, this username is copied through the msg buffer in the audit function and used in syslog, leaving room for a format string type vulnerability.
With that, we can write an arbitrary value in the parent's process memory, but we are constrained to a payload without null byte (as the username is copied with snprintf). Therefore, it is better to use only a single write.
Without stack leak, my solution was to rewrite the BPF filter inside the parent memory so that the next forked child would use hijacked seccomp rules:
This format string writes the word \xFF\x7F at pie_base + 0x5066, which patches the BPF filter in this part so that it always returns SECCOMP_RET_ALLOW:
Of course, the pie_base + 0x5066 address should not have a null byte in it (except for the most significant bytes), but this happens only very rarely. Right?

Final stage
Once the BPF filter has been hijacked, the server spawns a new child and a ret2libc concludes the challenge. For some reason it didn't work for me with system so I used execve.

As someone who's still relatively not at ease with pwn, I would like to thank the author of this challenge, which I found to be a fun ride with well designed steps.
Exploit script
Last updated
Was this helpful?