Docker breakout: SINCON 2020 Wonderland CTF
Earlier this weekend I had the opportunity to participate in SINCON 2020, a virtual cybersecurity conference organised by Infosec In The City and Div0. This conference was held over two days and featured talks, workshops, as well as two CTF competitions:
- One with a specific focus on cars
- The more conventional one (Wonderland CTF)
The 15 challenges as well as the infrastructure for Wonderland CTF were provided by Pentester Academy, who provided a very engaging and fun experience for us participants.
This post details the exploit procedure for one of the Hard difficulty challenges, involving Docker containers:
For those who are new to Docker, namespaces are a feature in the Linux kernel which Docker leverages on to provide isolation for (and between) containers. Placing containers in different PID namespaces, for instance, ensures that processes running in a container do not see processes in the host or in other containers. Capabilities, on the other hand, are a way to grant a subset of root privileges without needing to run processes as the root user. This also works in reverse — in the context of Docker, containers run as the root user by default, but capabilities are dropped in those containers, thus constraining the root user in the container.
The Docker documentation goes into greater detail on these (and more) security features.
Keeping that in mind, let’s launch the environment and see what we have:
We have root within the container (container 1), as well as a whole bunch of capabilities. Note that processes within container 1 have the additional CAP_SYS_PTRACE
capability that is not granted by default.
Running ps
reveals an interesting list of processes:
It is important to note here that per the mission description, there are two containers running which share the same PID namespace, therefore the above is actually a combined list of processes running on both container 1 and container 2 (undiscovered as of now).
Things which stand out here include the presence of supervisord, a process manager, as well as the Python SimpleHTTPServer, a simple web server which returns the contents of the directory it’s started in. A quick use of nc
(as well as guessing IP addresses) confirms that the HTTP server is not running on container 1 (172.17.0.3), but container 2 (172.17.0.2):
I was unable to find a use-case for the ptrace system call in container 1, however looking through the /proc filesystem reveals something interesting:
In addition to sharing the same PID namespace, the two containers are also sharing the user namespace. This means that root on container 1 = root on container 2.
To break out of container 1 and into container 2, we can (ab)use the /proc filesystem — specifically the /proc/<pid>/root entry — to gain access to the filesystem of the other container. As both containers sharing the same user namespace, we also have unrestricted access to the filesystem of container 2. More background is available in this blog post published by F-Secure Labs.
We perform a chroot into container 2’s filesystem and inspect the contents of the supervisord configuration:
Let’s replace this with a simple bind shell so that we can execute commands on container 2:
While chroot-ed into container 2’s filesystem, I also set the root password for that container and modified the container’s sshd configuration to permit root logins. This step is optional, however.
With the above done, we can now exit the chroot and send a SIGHUP to the supervisord process to reload its configuration. As a reminder, containers 1 and 2 share the same PID namespace, so both containers can see (and affect) each other’s processes.
A connection to port 8000 on container 2 confirms the ability to execute commands:
We start the sshd service on container 2 so that the later steps are easier to pull off:
Now let’s take a look at the capabilities granted to container 2:
This container has the CAP_SYS_MODULE
capability granted, which allows for the loading (and unloading) of modules to/from the host kernel.
At this point there are many possible ways to break out of the container. The method I used here comes from TheXcellerator’s escape
kernel module, which creates two new entries in /proc for command input and output, and uses the call_usermodehelper()
function to run a process in userland. I highly recommend that you give his blog post a read.
For this challenge, I only compiled TheXcellerator’s kernel module and skipped the userspace applications, which are not strictly necessary. The source code is available on GitHub for those who are interested. A simple Makefile is used for compilation:
…and now we load the module:
From here we can now execute any command on the host by sending the command to /proc/escape
, and read the command output from /proc/output
:
Based on previous challenges in the CTF, the flag is most likely located in /root
:
Takeaways
- Share namespaces between containers with caution
- Do not grant more capabilities than necessary to containers (the ol’ principle of least privilege) — the Internet is full of write-ups describing breakout attempts, even with other capabilities:
https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
https://medium.com/@fun_cuddles/docker-breakout-exploit-analysis-a274fff0e6b3 - OWASP also has a handy cheatsheet ready regarding Docker Security.