Everybody will tell you that a chroot jail (that is, making a process think that a directory is instead the root folder, and not letting it access or modify anything outside of that) is ineffective against a process with root privileges1 (UID 0). Let’s see why.
The escape basically works like this:
-
We create a temporary folder (I named mine
.42
, hidden not to draw too much attention) and wechroot
to that, this way we make sure our current working directory is outside the fake root, and we can do so because we’reCEOroot, Bitch2; -
then we
chroot
to parent folders all the way up to the root (we don’t need to worry about going too up,/../../.. == /
); -
finally we spawn something, a shell,
rm -rf
, whatever.
Q: Why couldn’t we just do chroot("../../../../../../..")
and call it a day?
A: Because even if the kernel does not want to keep us from doing what we want (we’re root, after all) it will keep faith to the chroot also with us and if from inside the chroot jail we ask to chroot("..")
the kernel will regularly expand /..
to /
. It has to do so, some programs might rely on that. So we have to move our working directory outside of the root before proceeding.
Other pitfalls
If chroot()
changes also the working directory to be inside the jail this will make it impossible to pop outside by just chrooting to a sub-directory, but this will not stop us.
We can simply grab the file descriptor of the current directory before the first chroot call and then fchdir()
to that. chroot()
does not close file descriptors.
Also, if the root privileges were incorrectly dropped, for example by calling seteuid()
, a call to setuid(0)
might be useful in restoring them.
So, how does a correct chroot look like?
1 2 3 4 |
|
And make sure that there are no setuid
binaries inside the jail3!