Abusing search permissions on Docker directories for privilege escalation

By Mohit Gupta

During a recent engagement, we came across an unfamiliar configuration pertaining to /var/lib/docker permissions. This, combined with a number of other lower risk issues, resulted in an attack path that allowed privilege escalation to root from a low-privileged user. 

As this configuration was non-standard rare to see, we thought we'd share our observations and methods for leveraging this particular weakness to the wider community. We will be doing this through an example, which was deployed to a WithSecure lab environment and only copied certain technical details observed on the engagement that are relevant to the attack path we will demonstrate. All users, containers, etc., are bespoke to this environment and do not represent the client's environment.

During our research, we did identify this was a previously identified issue with an assigned CVE (CVE-2021-41091) by CyberArk that they published when they were researching ways to get SUID binaries for privilege escalation.

The Environment

In this example, we will assume we have already found a method to obtain a low-privileged user. This is a standard Linux user called vagrant with a UID of 1000. The user has the ability to restart the host, but does not have any other special permissions otherwise. The host itself runs a number of containers through Docker.

Enumeration of the host reveals that the /var/lib/docker/ directory has the search bit set for "other". This can be seen below:

vagrant@ubuntu-noble:/var/lib$ ls -ld docker
drwx--x--x 14 root root 4096 Apr  4 15:46 docker

As a quick primer on Linux file permissions, there are three sets of three permissions in a base permission set. These are read, write and execute/search; each available for the owner, group and other entities. Note: the execute/search bit is execute for files and search for directories. Looking above, these permissions can be seen as rwx--x--x, which mean that the folder allows the following:

  • Read, write and search for the owner
  • Search for the group
  • Search for everyone else

The owner and group are the fields to the right of that and are root and root respectively. The other permissions would apply to all entities that are not either the owner or within the group. In this case, that would include our vagrant user as it is not the root user, or within the root group.

To list the contents of a directory the read permission is required; however, to change into a directory, only the search permission is required. Thus, the above permissions for /var/lib/docker/ mean that anyone can enter that directory, however most would not be able to list entries in the directory.

/var/lib/docker is the default data directory for Docker, containing details such as container configurations, filesystem layers, named volumes, etc. The directories within /var/lib/docker are generally consistent. Therefore, we can use another environment we control with docker installed, where we do have the required read privileges, to list the folders and then try our luck accessing any of those folders in the target environment. We speculated that similar permissions could have been assigned to child directories.

Testing out various folders revealed that overlay2, similarly, had the searchable bit set for other users, so a low-privileged user could also change into that directory. However, once again, they could not list the contents of the directory, as illustrated below:

vagrant@ubuntu-noble:/var/lib/docker$ ls
ls: cannot open directory '.': Permission denied
vagrant@ubuntu-noble:/var/lib/docker$ cd overlay2
vagrant@ubuntu-noble:/var/lib/docker/overlay2$ ls
ls: cannot open directory '.': Permission denied

Gaining access to a container

The overlay2 directory contains the filesystem layers used as part of the OverlayFS storage driver. This allows Docker to have multiple layers of an image, and merge them into one unified filesystem for the final container. The folder names of these layers are not easily guessable as they are each a sequence of 64 random hex characters.

Luckily, there is another manner by which to enumerate these folders. As they are mounted using an overlay filesystem, layers used for running containers can be found by running the mount command. An example is shown below:

vagrant@ubuntu-noble:/var/lib/docker/overlay2$ mount
[..SNIP..]
overlay on /var/lib/docker/overlay2/ac4[..SNIP..]901/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/43OI3BM5DA3SGUO7WC33YJF3EB:/var/lib/docker/overlay2/l/KORNYLM3EMNWOK4S6CKZDZHS2B:/var/lib/docker/overlay2/l/4BMQIUEFOULKIJAAEPHMC4QCMG:/var/lib/docker/overlay2/l/GZPCBKDDYJ3KN3EDVGHY7DDDZ5:/var/lib/docker/overlay2/l/OIE6CLK44XR2K57YFCTS7FWLET:/var/lib/docker/overlay2/l/XDHTSJG4JAZTNRJ5CMGZ6RLBYJ:/var/lib/docker/overlay2/l/5GAMZHRM2QDJ3B5YNFSASXSW52:/var/lib/docker/overlay2/l/RCHNXJAKPF52HY36SSEFSMFS3J,upperdir=/var/lib/docker/overlay2/ac4fe8077c737626dfacc1e823550dd675a02dfeef60f25cb6dc68e146861901/diff,workdir=/var/lib/docker/overlay2/ac4fe8077c737626dfacc1e823550dd675a02dfeef60f25cb6dc68e146861901/work,nouserxattr)

In the snippet above, there are multiple references to directories within /var/lib/docker/overlay2. The first one referenced is the merged folder, which is the culmination of all the layers. We tried changing into this directory as well, which worked.

vagrant@ubuntu-noble:/var/lib/docker/overlay2$ cd /var/lib/docker/overlay2/ac4[..SNIP..]901/merged
vagrant@ubuntu-noble:/var/lib/docker/overlay2/ac4[..SNIP..]901/merged$ ls
bin  boot  dev	etc  home  lib	lib32  lib64  libx32  media  mnt  opt  proc  root  run	sbin  srv  sys	tmp  usr  var

At this point, we have three areas where the searchable bit is set for "others" in the target environment:

  • /var/lib/docker
  • /var/lib/docker/overlay2
  • /var/lib/docker/overlay2/LAYER

This lets us, as a low-privileged user, access the filesystem of containers running on the host. It should be noted that these areas do not normally have this permission applied in current releases of Docker (as verified in our control environment).

Privilege Escalation

With access to these filesystems, we can perform further enumeration steps to see what we can find. Of particular interest was the fact that certain containers have application files owned by the same UID as our low-privileged user. This granted us read/write permissions on these files. By modifying these files, we could add malicious code to that, which, when run, would gain us a foothold within these containers. A command to enumerate all files owned by UID 1000 within all containers is shown below:

vagrant@ubuntu-noble:~$ mount | grep overlay | awk '{print $3}' | xargs -I {} find {} -uid 1000 2>/dev/null
/var/lib/docker/overlay2/790[..SNIP..]794/merged/code/app.py
/var/lib/docker/overlay2/790[..SNIP..]794/merged/docker-entrypoint.sh
[..SNIP..]

However, after updating a file, we would still need to restart the container for the new malicious code to be run. Luckily, one of the permissions granted to the low-privileged user is to reboot the host. This would result in the container processes being restarted, and thus loading the new malicious code from disk.

In the below example, we modified the docker-entrypoint.sh file to execute the id command and save the output before reverting to its normal operation.

vagrant@ubuntu-noble:/var/lib/docker/overlay2/db9[..SNIP..]e36/merged$ cat docker-entrypoint.sh
#! /bin/bash

python3 /code/app.py $@
vagrant@ubuntu-noble:/var/lib/docker/overlay2/db9[..SNIP..]e36/merged$ vim docker-entrypoint.sh
vagrant@ubuntu-noble:/var/lib/docker/overlay2/db9[..SNIP..]e36/merged$ cat docker-entrypoint.sh
#! /bin/bash

id >> /log

python3 /code/app.py $@

Once executed from a reboot, we can view the file to see the output of the command. Notably, some of the containers in the environment executed this injected code as the root user. Thus, we have code execution as root. However, this is limited to the container's namespace.

vagrant@ubuntu-noble:~$ cat /var/lib/docker/overlay2/db9[..SNIP..]e36/merged/log
uid=0(root) gid=0(root) groups=0(root)

Our next objective is to use our low-privileged access on the host and root privileges in the container to gain elevated access on the host itself, for which there are multiple approaches. One technique that could aid in this is leveraging mknod. This won't be discussed in this blog, as it has been discussed in detail previously.

Another would be to make a SUID binary as root within the container, that could then be executed by our low-privileged user to change our UID to 0.

However, we used a different technique during the engagement. One of the containers that we could gain a foothold in as the root user also had the Docker socket mounted in. Thus, this was used to create a new privileged container with a trivial breakout to gain root on the underlying host.

Conclusion

The setting of the searchable bit for other users on /var/lib/docker/ and child directories can allow for a low-privileged attacker to gain access to various containers' filesystems. This, at minimum, provides access to potentially sensitive material in the filesystem, for example mounted secrets; however, as shown in this post, it could also include privilege escalation risks.

While we were researching this issue further, we determined it was due to an older version of Docker being used. These excessive permissions were raised with the Docker project as CVE-2021-41091 by CyberArk, and subsequently remediated in Docker version 20.10.9.

For environments running out-of-date Docker instances, we recommend that these are updated to the latest stable version.

 

Related content

Abusing access to mount namespaces through /proc/pid/root

Containers are used to isolate workloads from the host system. In Linux, container runtimes such as Docker and LXC use multiple Linux namespaces to build an isolated environment for the workload.

Read more