docker unprivileged user / becoming root
My colleague was rightly annoyed that our USER www-data
docker
images greatly hindered effective debugging. Can we become root again,
while still keeping the additional secure-by-default non-root images?
If we have enough permissions on the filesystem, then: yes, we can.
Take the following example, where we’ll be looking at a myproject
pod. (You can skip the Kubernetes steps if you already know where
the Docker instance resides.)
$ kubectl get pods -o wide myproject-66dd6b4dd-jskgf
NAME READY STATUS AGE IP NODE
myproject-66dd6b4dd-jskgf 1/1 Running 64d 10.244.1.3 192.168.1.2
$ kubectl exec -it myproject-66dd6b4dd-jskgf -- bash
myproject-66dd6b4dd-jskgf:/app$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Yes, so indeed, we are not root. If we want to install ping
or
curl
or some other useful library, our apt-get
powers are limited.
Go the the node, find the docker instance, inspect it to find the “current” filesystem:
$ ssh 192.168.1.2
NODE# docker ps | grep myproject
550480e3a8a7 myhub/myproject-img "uwsgi uwsgi.ini"
k8s_web_myproject-66dd6b4dd-jskgf_ns
NODE# docker inspect k8s_web_myproject-66dd6b4dd-jskgf_ns |
grep Graph -A10
"GraphDriver": {
"Data": {
"Dataset": "rpool/ROOT/ubuntu/b6846..",
"Mountpoint": "/var/lib/docker/zfs/graph/b6846.."
},
"Name": "zfs"
},
...
The above example shows a ZFS filesystem. For Overlay2 it may look like this:
NODE# docker inspect k8s_web_myproject-66dd6b4dd-jskgf_ns |
grep Graph -A10
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/ff5b1c..-init/diff:/var/..",
"MergedDir": "/var/lib/docker/overlay2/ff5b1c../merged",
"UpperDir": "/var/lib/docker/overlay2/ff5b1c../diff",
"WorkDir": "/var/lib/docker/overlay2/ff5b1c../work"
},
"Name": "overlay2"
},
...
We should be able to find the instance filesystem in the Mountpoint:
NODE# ls -log /var/lib/docker/zfs/graph/b6846../bin/bash
-rwxr-xr-x 1 1037528 Jul 12 2019 /var/lib/docker/zfs/graph/b6846../bin/bash
Or, for Overlay2, in the MergedDir:
NODE# ls -log /var/lib/docker/overlay2/ff5b1c../merged/bin/bash
-rwxr-xr-x 1 1113504 Jun 7 2019 \
/var/lib/docker/overlay2/ff5b1c../merged/bin/bash
On to the actual changes…
Create a temporary setuid binary. You could choose any binary, but
you’ll need something that allows you to go from effective uid to real
uid. /bin/sh
or /bin/bash
is a safe bet, you can program your way
upwards from there:
NODE# install -oroot -m4775 \
/var/lib/docker/zfs/graph/b6846../bin/{bash,superbash}
Now we should have a copy of /bin/bash
as /bin/superbash
with mode
4755
(setuid bit).
Go back to the docker instance and start superbash
— bash needs
-p
to refrain from dropping the effective uid:
$ kubectl exec -it myproject-66dd6b4dd-jskgf -- bash
myproject-66dd6b4dd-jskgf:/app$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
myproject-66dd6b4dd-jskgf:/app$ superbash -p
superbash-4.3# id
uid=33(www-data) gid=33(www-data) euid=0(root) groups=33(www-data)
Almost there, we now have effective root but not real root. Use your favorite scripting language to finish it off:
superbash-4.3# python -c \
'from os import *;setuid(0);setgid(0);b="/bin/bash";execve(b,[b],environ)'
myproject-66dd6b4dd-jskgf:/app# id
uid=0(root) gid=0(root) groups=0(root)
Remove the superbash
backdoor when you’re done.
myproject-66dd6b4dd-jskgf:/app# rm /bin/superbash
Or better yet, restart the docker image to flush your changes.