diff options
Diffstat (limited to 'usr/src/lib/libshell/common/sh/suid_exec.c')
-rw-r--r-- | usr/src/lib/libshell/common/sh/suid_exec.c | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/usr/src/lib/libshell/common/sh/suid_exec.c b/usr/src/lib/libshell/common/sh/suid_exec.c new file mode 100644 index 0000000000..0495d29890 --- /dev/null +++ b/usr/src/lib/libshell/common/sh/suid_exec.c @@ -0,0 +1,509 @@ +/*********************************************************************** +* * +* This software is part of the ast package * +* Copyright (c) 1982-2007 AT&T Knowledge Ventures * +* and is licensed under the * +* Common Public License, Version 1.0 * +* by AT&T Knowledge Ventures * +* * +* 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 <dgk@research.att.com> * +* * +***********************************************************************/ +#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 <ast.h> +#include "FEATURE/externs" +#include <ls.h> +#include <sig.h> +#include <error.h> +#include <sys/wait.h> +#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 <dir> 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 <template> + */ + +static void maketemp(char *template) +{ + register char *cp = template; + register pid_t n = getpid(); + /* skip to end of string */ + while(*++cp); + /* convert process id to string */ + while(n > 0) + { + *--cp = (n%10) + '0'; + n /= 10; + } + +} + +/* + * copy THISPROG into the open file number <fdo> and close <fdo> + */ + +static int mycopy(int fdi, int fdo) +{ + char buffer[BLKSIZE]; + register int n; + + while((n = read(fdi,buffer,BLKSIZE)) > 0) + if(write(fdo,buffer,n) != n) + break; + close(fdi); + close(fdo); + return n; +} + +#endif /* _lib_setreuid */ + + |