Search This Blog

Thursday, August 26, 2021

corCTF 2021 ret2cds writeup: Escaping a Seccomp Sandbox via Class Data Sharing regions in OpenJDK

This year, my team hosted a very successful corCTF! Though we did make it much more difficult than expected, we still received overwhelmingly positive reviews. I thought it would be a good idea to make some writeups for the challenges I designed, which included ret2cds (pwn, 6 solves), vmquack (rev, 3 solves), and the series of kernel challenges designed by D3v17 and me (Fire of Salvation and Wall of Perdition); unfortunately, despite teams coming very close, the difficulty of other pwn tasks combined (firefox pwn, and two novel heap note challenges on glibc 2.32 and 2.34) caused there to be 0 solves in 48 hours. For this post, I will detail a brief writeup for the ret2cds challenge, in which one must abuse the OpenJDK Class Data Sharing region in the netcat process (rewritten in java to behave like a xinetd service or socat process) to escape the sandbox and pop a reverse shell.

Oftentimes during CTFs, pwners might joke about burning their socat or xinetd 0 days on pwnables that are deemed quite insane. This idea is what inspired me to make this challenge, Opening up the ret2cds binary in IDA shows the following:

There is a pretty clear overflow here and can lead to a ret2libc attack. However, in one of the constructor functions before main, a seccomp sandbox is initiated blacklisting every syscall but read, write, mprotect, mmap, munmap, process_vm_readv, process_vm_writev, exit, exit_group, gettimeofday, reboot. Most of these aren't very useful for popping a shell to get the flag, except for the process_vm_readv/writev family of syscalls. Looking at the provided docker files, we see that the ret2cds is hosted by the following under the user ret2cds:

while true; do java -jar /opt/nc-java/nc-java.jar ./ret2cds 1337; done

The manpages for these two syscalls are very helpful (process_vm_readv, process_vm_writev). Basically, these two syscalls allow transfer of data between local and remote processes governed by the PTRACE_MODE_ATTACH_REALCREDS check. It's also important to note that during my fiddling, unlike the PEEK and POKE family of options in PTRACE, these syscalls respect the remote process's memory map permissions. Since the java netcat is probably the only thing running under the same uid under the docker, its pid can be enumerated by bruteforcing pids with a potential known address in its process by checking the return value of process_vm_readv. Now, what is a fixed address we can abuse, and where can we write to?

On my first time ever attaching to that process, I saw the following page permissions in the beginning.

An rwx region! And after a few more new instances of the process, it's also at a fixed location! This sounds like a perfect address to use for pid enumeration and for attacking with process_vm_writev. At this point, I was somewhat curious what this rwx region in openjdk is for. Before even knowing what it actually did, I just wrote a sample execve("/bin/sh", 0, 0); shellcode into that region, and a shell popped in the nc-java process with nothing crashing.

I suspected perhaps it was some JIT region, but digging deeply into OpenJDK source with my teammate chop0, we came across the following source file. Some interesting observations were that in the comments, they mentioned that 0x800000000 is the default address for this class data sharing section, which is just used to reduce loadup times for smaller jar applications (such as by archiving commonly loaded class data and having it shared between multiple JVMs). The first region is also used as storage for both trampoline code portions and C++ vtables. When looking at how OpenJDK initializes this region in memory, we the following line (it allocates the first three pages for the aforementioned region first, then a rw region, and finally a ro region). Interesting... a region mixing code with vtables and with read_only set to False, a curious implementation to say the least...

Intuitively, as a pwner, when I see an rwx region, I just write to it, hoping it just works before debugging. In this case, it worked very well as I mentioned earlier, even with a reverse shell payload once you reconnect to the nc java handler. 

Here's my final exploit:

I used the first program to dump the shellcode that would inject into the CDS rwx region of the nc-java process. A brief nop sled was attached as otherwise the exploit seemed somewhat unstable (since the misc code that gets executed upon a reconnect might not start exactly at the beginning of the region). Then, I used the second python pwntool script to pwn the ret2cds binary, to then inject the shellcode and pop a reverse shell (I just borrowed my favorite one from shellstorm).

Once you run this, it should pop a reverse shell in return with the following flag: corCTF{r0p_t0_5h3llc0d3_t0_pWn1n1g_j@v@_rwX_cDs!!!}

I was quite surprised at the low solve count. I thought that the blacklisted syscalls would make it quite obvious which syscalls to abuse, and hence look at what are good places to target in the java JVM. Most people who solved also agreed with me regarding the difficulty, but did mention that the description, which talked about java and mentioned an issue with running the docker under Ubuntu, worried players about a very complex exploitation path. In the end, it still seemed like a pretty fun challenge (as we needed another ROP challenge besides just from a ret2libc) that showcased some interesting behavior about OpenJDK (do note that newer versions of OpenJDK seem to be safe from this behavior).

No comments:

Post a Comment