docker unprivileged user / becoming root

docker unprivileged user / becoming root

  • Written by
    Walter Doekes
  • Published on

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.


Back to overview Newer post: libreoffice / asking for cell password / brute force Older post: nss-dns4only / libc / disable AAAA lookups