/*********************************************************************** * * * This software is part of the ast package * * Copyright (c) 1982-2008 AT&T Intellectual Property * * and is licensed under the * * Common Public License, Version 1.0 * * by AT&T Intellectual Property * * * * A copy of the License is available at * * http://www.opensource.org/licenses/cpl1.0.txt * * (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9) * * * * Information and Software Systems Research * * AT&T Research * * Florham Park NJ * * * * David Korn * * * ***********************************************************************/ #pragma prototyped /* * This is a program to execute 'execute only' and suid/sgid shell scripts. * This program must be owned by root and must have the set uid bit set. * It must not have the set group id bit set. This program must be installed * where the define parameter THISPROG indicates to work correctly on system V * * Written by David Korn * AT&T Labs * Enhanced by Rob Stampfli */ /* The file name of the script to execute is argv[0] * Argv[1] is the program name * The basic idea is to open the script as standard input, set the effective * user and group id correctly, and then exec the shell. * The complicated part is getting the effective uid of the caller and * setting the effective uid/gid. The program which execs this program * may pass file descriptor FDIN as an open file with mode SPECIAL if * the effective user id is not the real user id. The effective * user id for authentication purposes will be the owner of this * open file. On systems without the setreuid() call, e[ug]id is set * by copying this program to a /tmp/file, making it a suid and/or sgid * program, and then execing this program. * A forked version of this program waits until it can unlink the /tmp * file and then exits. Actually, we fork() twice so the parent can * wait for the child to complete. A pipe is used to guarantee that we * do not remove the /tmp file too soon. */ #include #include "FEATURE/externs" #include #include #include #include #include "version.h" #define SPECIAL 04100 /* setuid execute only by owner */ #define FDIN 10 /* must be same as /dev/fd below */ #undef FDSYNC #define FDSYNC 11 /* used on sys5 to synchronize cleanup */ #define FDVERIFY 12 /* used to validate /tmp process */ #undef BLKSIZE #define BLKSIZE sizeof(char*)*1024 #define THISPROG "/etc/suid_exec" #define DEFSHELL "/bin/sh" static void error_exit(const char*); static int in_dir(const char*, const char*); static int endsh(const char*); #ifndef _lib_setregid # undef _lib_setreuid #endif #ifndef _lib_setreuid static void setids(int,uid_t,gid_t); static int mycopy(int, int); static void maketemp(char*); #else static void setids(int,int,int); #endif /* _lib_setreuid */ static const char version[] = "\n@(#)$Id: suid_exec "SH_RELEASE" $\n"; static const char badopen[] = "cannot open"; static const char badexec[] = "cannot exec"; static const char devfd[] = "/dev/fd/10"; /* must match FDIN above */ static char tmpname[] = "/tmp/SUIDXXXXXX"; static char **arglist; static char *shell; static char *command; static uid_t ruserid; static uid_t euserid; static gid_t rgroupid; static gid_t egroupid; static struct stat statb; int main(int argc,char *argv[]) { register int m,n; register char *p; struct stat statx; int mode; uid_t effuid; gid_t effgid; NOT_USED(argc); arglist = argv; if((command = argv[1]) == 0) error_exit(badexec); ruserid = getuid(); euserid = geteuid(); rgroupid = getgid(); egroupid = getegid(); p = argv[0]; #ifndef _lib_setreuid maketemp(tmpname); if(strcmp(p,tmpname)==0) { /* At this point, the presumption is that we are the * version of THISPROG copied into /tmp, with the owner, * group, and setuid/gid bits correctly set. This copy of * the program is executable by anyone, so we must be careful * not to allow just any invocation of it to succeed, since * it is setuid/gid. Validate the proper execution by * examining the FDVERIFY file descriptor -- if it is owned * by root and is mode SPECIAL, then this is proof that it was * passed by a program with superuser privileges -- hence we * can presume legitimacy. Otherwise, bail out, as we suspect * an impostor. */ if(fstat(FDVERIFY,&statb) < 0 || statb.st_uid != 0 || (statb.st_mode & ~S_IFMT) != SPECIAL || close(FDVERIFY)<0) error_exit(badexec); /* This enables the grandchild to clean up /tmp file */ close(FDSYNC); /* Make sure that this is a valid invocation of the clone. * Perhaps unnecessary, given FDVERIFY, but what the heck... */ if(stat(tmpname,&statb) < 0 || statb.st_nlink != 1 || !S_ISREG(statb.st_mode)) error_exit(badexec); if(ruserid != euserid && ((statb.st_mode & S_ISUID) == 0 || statb.st_uid != euserid)) error_exit(badexec); goto exec; } /* Make sure that this is the real setuid program, not the clone. * It is possible by clever hacking to get past this point in the * clone, but it doesn't do the hacker any good that I can see. */ if(euserid) error_exit(badexec); #endif /* _lib_setreuid */ /* Open the script for reading first and then validate it. This * prevents someone from pulling a switcheroo while we are validating. */ n = open(p,0); if(n == FDIN) { n = dup(n); close(FDIN); } if(n < 0) error_exit(badopen); /* validate execution rights to this script */ if(fstat(FDIN,&statb) < 0 || (statb.st_mode & ~S_IFMT) != SPECIAL) euserid = ruserid; else euserid = statb.st_uid; /* do it the easy way if you can */ if(euserid == ruserid && egroupid == rgroupid) { if(access(p,X_OK) < 0) error_exit(badexec); } else { /* have to check access on each component */ while(*p++) { if(*p == '/' || *p == 0) { m = *p; *p = 0; if(eaccess(argv[0],X_OK) < 0) error_exit(badexec); *p = m; } } p = argv[0]; } if(fstat(n, &statb) < 0 || !S_ISREG(statb.st_mode)) error_exit(badopen); if(stat(p, &statx) < 0 || statb.st_ino != statx.st_ino || statb.st_dev != statx.st_dev) error_exit(badexec); if(stat(THISPROG, &statx) < 0 || (statb.st_ino == statx.st_ino && statb.st_dev == statx.st_dev)) error_exit(badexec); close(FDIN); if(fcntl(n,F_DUPFD,FDIN) != FDIN) error_exit(badexec); close(n); /* compute the desired new effective user and group id */ effuid = euserid; effgid = egroupid; mode = 0; if(statb.st_mode & S_ISUID) effuid = statb.st_uid; if(statb.st_mode & S_ISGID) effgid = statb.st_gid; /* see if group needs setting */ if(effgid != egroupid) if(effgid != rgroupid || setgid(rgroupid) < 0) mode = S_ISGID; /* now see if the uid needs setting */ if(mode) { if(effuid != ruserid) mode |= S_ISUID; } else if(effuid) { if(effuid != ruserid || setuid(ruserid) < 0) mode = S_ISUID; } if(mode) setids(mode, effuid, effgid); #ifndef _lib_setreuid exec: #endif /* _lib_setreuid */ /* only use SHELL if file is in trusted directory and ends in sh */ shell = getenv("SHELL"); if(shell == 0 || !endsh(shell) || ( !in_dir("/bin",shell) && !in_dir("/usr/bin",shell) && !in_dir("/usr/lbin",shell) && !in_dir("/usr/local/bin",shell))) shell = DEFSHELL; argv[0] = command; argv[1] = (char*)devfd; execv(shell,argv); error_exit(badexec); } /* * return true of shell ends in sh of ksh */ static int endsh(register const char *shell) { while(*shell) shell++; if(*--shell != 'h' || *--shell != 's') return(0); if(*--shell=='/') return(1); if(*shell=='k' && *--shell=='/') return(1); return(0); } /* * return true of shell is in directory */ static int in_dir(register const char *dir,register const char *shell) { while(*dir) { if(*dir++ != *shell++) return(0); } /* return true if next character is a '/' */ return(*shell=='/'); } static void error_exit(const char *message) { sfprintf(sfstdout,"%s: %s\n",command,message); exit(126); } /* * This version of access checks against effective uid and effective gid */ int eaccess(register const char *name, register int mode) { struct stat statb; if (stat(name, &statb) == 0) { if(euserid == 0) { if(!S_ISREG(statb.st_mode) || mode != 1) return(0); /* root needs execute permission for someone */ mode = (S_IXUSR|S_IXGRP|S_IXOTH); } else if(euserid == statb.st_uid) mode <<= 6; else if(egroupid == statb.st_gid) mode <<= 3; #ifdef _lib_getgroups /* on some systems you can be in several groups */ else { static int maxgroups; gid_t *groups=0; register int n; if(maxgroups==0) { /* first time */ if((maxgroups=getgroups(0,groups)) < 0) { /* pre-POSIX system */ maxgroups=NGROUPS_MAX; } } groups = (gid_t*)malloc((maxgroups+1)*sizeof(gid_t)); n = getgroups(maxgroups,groups); while(--n >= 0) { if(groups[n] == statb.st_gid) { mode <<= 3; break; } } } #endif /* _lib_getgroups */ if(statb.st_mode & mode) return(0); } return(-1); } #ifdef _lib_setreuid static void setids(int mode,int owner,int group) { if(mode & S_ISGID) setregid(rgroupid,group); /* set effective uid even if S_ISUID is not set. This is because * we are *really* executing EUID root at this point. Even if S_ISUID * is not set, the value for owner that is passsed should be correct. */ setreuid(ruserid,owner); } #else /* * This version of setids creats a /tmp file and copies itself into it. * The "clone" file is made executable with appropriate suid/sgid bits. * Finally, the clone is exec'ed. This file is unlinked by a grandchild * of this program, who waits around until the text is free. */ static void setids(int mode,uid_t owner,gid_t group) { register int n,m; int pv[2]; /* * Create a token to pass to the new program for validation. * This token can only be procured by someone running with an * effective userid of root, and hence gives the clone a way to * certify that it was really invoked by THISPROG. Someone who * is already root could spoof us, but why would they want to? * * Since we are root here, we must be careful: What if someone * linked a valuable file to tmpname? */ unlink(tmpname); /* should normally fail */ #ifdef O_EXCL if((n = open(tmpname, O_WRONLY | O_CREAT | O_EXCL, SPECIAL)) < 0 || unlink(tmpname) < 0) #else if((n = open(tmpname, O_WRONLY | O_CREAT ,SPECIAL)) < 0 || unlink(tmpname) < 0) #endif error_exit(badexec); if(n != FDVERIFY) { close(FDVERIFY); if(fcntl(n,F_DUPFD,FDVERIFY) != FDVERIFY) error_exit(badexec); } mode |= S_IEXEC|(S_IEXEC>>3)|(S_IEXEC>>6); /* create a pipe for synchronization */ if(pipe(pv) < 0) error_exit(badexec); if((n=fork()) == 0) { /* child */ close(FDVERIFY); close(pv[1]); if((n=fork()) == 0) { /* grandchild -- cleans up clone file */ signal(SIGHUP, SIG_IGN); signal(SIGINT, SIG_IGN); signal(SIGQUIT, SIG_IGN); signal(SIGTERM, SIG_IGN); read(pv[0],pv,1); /* wait for clone to close pipe */ while(unlink(tmpname) < 0 && errno == ETXTBSY) sleep(1); exit(0); } else if(n == -1) exit(1); else { /* Create a set[ug]id file that will become the clone. * To make this atomic, without need for chown(), the * child takes on desired user and group. The only * downsize of this that I can see is that it may * screw up some per- * user accounting. */ if((m = open(THISPROG, O_RDONLY)) < 0) exit(1); if((mode & S_ISGID) && setgid(group) < 0) exit(1); if((mode & S_ISUID) && owner && setuid(owner) < 0) exit(1); #ifdef O_EXCL if((n = open(tmpname,O_WRONLY|O_CREAT|O_TRUNC|O_EXCL, mode)) < 0) #else unlink(tmpname); if((n = open(tmpname,O_WRONLY|O_CREAT|O_TRUNC, mode)) < 0) #endif /* O_EXCL */ exit(1); /* populate the clone */ m = mycopy(m,n); if(chmod(tmpname,mode) <0) exit(1); exit(m); } } else if(n == -1) error_exit(badexec); else { arglist[0] = (char*)tmpname; close(pv[0]); /* move write end of pipe into FDSYNC */ if(pv[1] != FDSYNC) { close(FDSYNC); if(fcntl(pv[1],F_DUPFD,FDSYNC) != FDSYNC) error_exit(badexec); } /* wait for child to die */ while((m = wait(0)) != n) if(m == -1 && errno != EINTR) break; /* Kill any setuid status at this point. That way, if the * clone is not setuid, we won't exec it as root. Also, don't * neglect to consider that someone could have switched the * clone file on us. */ if(setuid(ruserid) < 0) error_exit(badexec); execv(tmpname,arglist); error_exit(badexec); } } /* * create a unique name into the