Docker is fun

There was a problem on a server we did not control. It was managed by a third party and we only got a service account. Since things were down and I did not have full root access I got a bit annoyed waiting for them to respond back.

I decided to take matters into my own hands. So first things first, we have sudo privileges but you need to know the password. We do not know the password, everything is done with SSH keys. Secondly there is docker on the system but we can only interact with it via their homegrown tool. Hmmz, how can we exploit this?

A special kind of binary

There are binaries in Linux that are marked s for special, or setuid, either or. These kinds of binaries run with what is known as a different effective uid or euid. For example sudo is one of these programs.

-rwsr-xr-x 1 root root 187K Feb 18  2021 /usr/bin/sudo

That s in the -rws means the setuid bit is set. Of course that is how sudo works, it sets the real uid to the euid and then executes whatever program needed. We can write our own software that does this without asking for passwords.

Take note however of the fact that the euid comes from who actually owns the binary. So you need root privileges in order to make a binary owned by root and so if you already have that then you don't need this exploit.

Go make a binary

So let me try my hand at writing it in Go. The filename is stars.go.

package main

import "syscall"


func main() {
  syscall.Setuid(syscall.Geteuid())
  syscall.Exec("/bin/sh", []string{}, []string{})
}

That is all it takes. Really, that is it. So we build the binary with go build stars.go and then the following on a machine you control to test it out:

sudo chown root:root stars
sudo chmod u+s stars

This creates a binary that has the setuid bit and owned by root but anyone can execute and so run it and BAM we got a root shell.

Well not quite. It did not work for me. So I ditched this way.

Later on I will share what failed to make it work.

Good ol' C

C never fails. So let us write some C code that does the exact same thing.

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

static uid_t euid;

int main (void) {
  euid = geteuid();
  setuid(euid);
  char* args[] = {"/bin/sh", NULL};
  execvp(args[0], args);
}

As you can see it is not a lot more that is needed to be written. Compile with gcc -o stars stars.c and repeat the same steps above for making the binary the correct state. Run it and BAM we do get a root shell. This was so exciting for me. The proof of concept (PoC) worked.

Actual operation

Back to the server. Since there is access to docker via a docker-compose we create the following docker-compose.yml file:

version: '3.3'

services:
  generic:
      image: rockylinux/rockylinux
      command:
         - "/usr/bin/tail"
         - "-f"
         - "/dev/null"
      volumes:
         - .:/data

Then run the command via their tool to get a container infinitely running and then exec into it. The reason for the RockyLinux container is because I was on CentOS host machine and I wanted to see how RockyLinux was whilst staying relatively close to the host machine. The command is needed otherwise the container will exit immediately. This command is basically trying to endlessly read /dev/null but that will never produce output.

Now navigate to /data and install vim and gcc. I did this with yum but I realized now that actually that should have been dnf, but maybe underwater they are symlinked.

Then after installation create the stars.c file and compile it and fix the binary. Then drop out of the docker container and run the binary on the host system and BAM, root shell, pwned, h4xx0rd and what have you not.

Quickly edited the /etc/sudoers file to give me passwordless sudo and if you really wanted to be fancy you could edit out your wtmp and utmp entries to cover your tracks but I did not feel this was necessary.

Go make a binary, again

So I wanted to get it to work with Go though as I did not understand why it failed. I looked at the official docs and saw it gave back an error object. I thought let me log that out and see what it contains. It contained something along the lines of “operation not permitted”. So a quick search on that with Setuid call and I got the result that Go version 1.15 had a bug that made it not work but Go 1.16 and up had it fixed. This commit has the exact reason.

So I installed Go 1.17 and recompiled and reran and BAM, I got a shell again.

The advantage of making a Golang exploit is you automatically get a statically linked binary that could just be dropped in as long as they ran the general same version of the C library, for example GNU libc, and have the same architecture (both being x86_64 or aarch64 for example) .

So if you wanted to target Alpine (musl instead of glibc) running on Raspberry Pi 4 (aarch64 instead of x86) then you have to cross compile or use some clever tricks I will write in the next post of running docker as a different architecture using QEMU.

Conclusion

This was a fun and simple way of getting around the no root thing. Just remember that Docker shares the kernel with the host and therefore you can easily bypass many restrictions you try to put on your system. For instance making /etc/sudoers read-only (I have root, I can do what I want...) or never sharing passwords only SSH keys.

How to combat this? Well you could use rootless docker that does not do user mapping with the containers. If you do not map users 1-1 with the host you can never actually be root on the host machine and therefore your root user in the container can not create these special binaries on the machine itself.

#devlife #devops #devsecops #secops