/***********************************************************************1* *2* This software is part of the ast package *3* Copyright (c) 1982-2011 AT&T Intellectual Property *4* and is licensed under the *5* Eclipse Public License, Version 1.0 *6* by AT&T Intellectual Property *7* *8* A copy of the License is available at *9* http://www.eclipse.org/org/documents/epl-v10.html *10* (with md5 checksum b35adb5213ca9657e911e9befb180842) *11* *12* Information and Software Systems Research *13* AT&T Research *14* Florham Park NJ *15* *16* David Korn <[email protected]> *17* *18***********************************************************************/19#pragma prototyped20/*21* This is a program to execute 'execute only' and suid/sgid shell scripts.22* This program must be owned by root and must have the set uid bit set.23* It must not have the set group id bit set. This program must be installed24* where the define parameter THISPROG indicates to work correctly on system V25*26* Written by David Korn27* AT&T Labs28* Enhanced by Rob Stampfli29*/3031/* The file name of the script to execute is argv[0]32* Argv[1] is the program name33* The basic idea is to open the script as standard input, set the effective34* user and group id correctly, and then exec the shell.35* The complicated part is getting the effective uid of the caller and36* setting the effective uid/gid. The program which execs this program37* may pass file descriptor FDIN as an open file with mode SPECIAL if38* the effective user id is not the real user id. The effective39* user id for authentication purposes will be the owner of this40* open file. On systems without the setreuid() call, e[ug]id is set41* by copying this program to a /tmp/file, making it a suid and/or sgid42* program, and then execing this program.43* A forked version of this program waits until it can unlink the /tmp44* file and then exits. Actually, we fork() twice so the parent can45* wait for the child to complete. A pipe is used to guarantee that we46* do not remove the /tmp file too soon.47*/4849#include <ast.h>50#include "FEATURE/externs"51#include <ls.h>52#include <sig.h>53#include <error.h>54#include <sys/wait.h>55#include "version.h"5657#define SPECIAL 04100 /* setuid execute only by owner */58#define FDIN 10 /* must be same as /dev/fd below */59#undef FDSYNC60#define FDSYNC 11 /* used on sys5 to synchronize cleanup */61#define FDVERIFY 12 /* used to validate /tmp process */62#undef BLKSIZE63#define BLKSIZE sizeof(char*)*102464#define THISPROG "/etc/suid_exec"65#define DEFSHELL "/bin/sh"6667static void error_exit(const char*);68static int in_dir(const char*, const char*);69static int endsh(const char*);70#ifndef _lib_setregid71# undef _lib_setreuid72#endif73#ifndef _lib_setreuid74static void setids(int,uid_t,gid_t);75static int mycopy(int, int);76static void maketemp(char*);77#else78static void setids(int,int,int);79#endif /* _lib_setreuid */8081static const char version[] = "\n@(#)$Id: suid_exec "SH_RELEASE" $\n";82static const char badopen[] = "cannot open";83static const char badexec[] = "cannot exec";84static const char devfd[] = "/dev/fd/10"; /* must match FDIN above */85static char tmpname[] = "/tmp/SUIDXXXXXX";86static char **arglist;8788static char *shell;89static char *command;90static uid_t ruserid;91static uid_t euserid;92static gid_t rgroupid;93static gid_t egroupid;94static struct stat statb;9596int main(int argc,char *argv[])97{98register int m,n;99register char *p;100struct stat statx;101int mode;102uid_t effuid;103gid_t effgid;104NOT_USED(argc);105arglist = argv;106if((command = argv[1]) == 0)107error_exit(badexec);108ruserid = getuid();109euserid = geteuid();110rgroupid = getgid();111egroupid = getegid();112p = argv[0];113#ifndef _lib_setreuid114maketemp(tmpname);115if(strcmp(p,tmpname)==0)116{117/* At this point, the presumption is that we are the118* version of THISPROG copied into /tmp, with the owner,119* group, and setuid/gid bits correctly set. This copy of120* the program is executable by anyone, so we must be careful121* not to allow just any invocation of it to succeed, since122* it is setuid/gid. Validate the proper execution by123* examining the FDVERIFY file descriptor -- if it is owned124* by root and is mode SPECIAL, then this is proof that it was125* passed by a program with superuser privileges -- hence we126* can presume legitimacy. Otherwise, bail out, as we suspect127* an impostor.128*/129if(fstat(FDVERIFY,&statb) < 0 || statb.st_uid != 0 ||130(statb.st_mode & ~S_IFMT) != SPECIAL || close(FDVERIFY)<0)131error_exit(badexec);132/* This enables the grandchild to clean up /tmp file */133close(FDSYNC);134/* Make sure that this is a valid invocation of the clone.135* Perhaps unnecessary, given FDVERIFY, but what the heck...136*/137if(stat(tmpname,&statb) < 0 || statb.st_nlink != 1 ||138!S_ISREG(statb.st_mode))139error_exit(badexec);140if(ruserid != euserid &&141((statb.st_mode & S_ISUID) == 0 || statb.st_uid != euserid))142error_exit(badexec);143goto exec;144}145/* Make sure that this is the real setuid program, not the clone.146* It is possible by clever hacking to get past this point in the147* clone, but it doesn't do the hacker any good that I can see.148*/149if(euserid)150error_exit(badexec);151#endif /* _lib_setreuid */152/* Open the script for reading first and then validate it. This153* prevents someone from pulling a switcheroo while we are validating.154*/155n = open(p,0);156if(n == FDIN)157{158n = dup(n);159close(FDIN);160}161if(n < 0)162error_exit(badopen);163/* validate execution rights to this script */164if(fstat(FDIN,&statb) < 0 || (statb.st_mode & ~S_IFMT) != SPECIAL)165euserid = ruserid;166else167euserid = statb.st_uid;168/* do it the easy way if you can */169if(euserid == ruserid && egroupid == rgroupid)170{171if(access(p,X_OK) < 0)172error_exit(badexec);173}174else175{176/* have to check access on each component */177while(*p++)178{179if(*p == '/' || *p == 0)180{181m = *p;182*p = 0;183if(eaccess(argv[0],X_OK) < 0)184error_exit(badexec);185*p = m;186}187}188p = argv[0];189}190if(fstat(n, &statb) < 0 || !S_ISREG(statb.st_mode))191error_exit(badopen);192if(stat(p, &statx) < 0 ||193statb.st_ino != statx.st_ino || statb.st_dev != statx.st_dev)194error_exit(badexec);195if(stat(THISPROG, &statx) < 0 ||196(statb.st_ino == statx.st_ino && statb.st_dev == statx.st_dev))197error_exit(badexec);198close(FDIN);199if(fcntl(n,F_DUPFD,FDIN) != FDIN)200error_exit(badexec);201close(n);202203/* compute the desired new effective user and group id */204effuid = euserid;205effgid = egroupid;206mode = 0;207if(statb.st_mode & S_ISUID)208effuid = statb.st_uid;209if(statb.st_mode & S_ISGID)210effgid = statb.st_gid;211212/* see if group needs setting */213if(effgid != egroupid)214if(effgid != rgroupid || setgid(rgroupid) < 0)215mode = S_ISGID;216217/* now see if the uid needs setting */218if(mode)219{220if(effuid != ruserid)221mode |= S_ISUID;222}223else if(effuid)224{225if(effuid != ruserid || setuid(ruserid) < 0)226mode = S_ISUID;227}228229if(mode)230setids(mode, effuid, effgid);231#ifndef _lib_setreuid232exec:233#endif /* _lib_setreuid */234/* only use SHELL if file is in trusted directory and ends in sh */235shell = getenv("SHELL");236if(shell == 0 || !endsh(shell) || (237!in_dir("/bin",shell) &&238!in_dir("/usr/bin",shell) &&239!in_dir("/usr/lbin",shell) &&240!in_dir("/usr/local/bin",shell)))241shell = DEFSHELL;242argv[0] = command;243argv[1] = (char*)devfd;244execv(shell,argv);245error_exit(badexec);246}247248/*249* return true of shell ends in sh of ksh250*/251252static int endsh(register const char *shell)253{254while(*shell)255shell++;256if(*--shell != 'h' || *--shell != 's')257return(0);258if(*--shell=='/')259return(1);260if(*shell=='k' && *--shell=='/')261return(1);262return(0);263}264265266/*267* return true of shell is in <dir> directory268*/269270static int in_dir(register const char *dir,register const char *shell)271{272while(*dir)273{274if(*dir++ != *shell++)275return(0);276}277/* return true if next character is a '/' */278return(*shell=='/');279}280281static void error_exit(const char *message)282{283sfprintf(sfstdout,"%s: %s\n",command,message);284exit(126);285}286287288/*289* This version of access checks against effective uid and effective gid290*/291292int eaccess(register const char *name, register int mode)293{294struct stat statb;295if (stat(name, &statb) == 0)296{297if(euserid == 0)298{299if(!S_ISREG(statb.st_mode) || mode != 1)300return(0);301/* root needs execute permission for someone */302mode = (S_IXUSR|S_IXGRP|S_IXOTH);303}304else if(euserid == statb.st_uid)305mode <<= 6;306else if(egroupid == statb.st_gid)307mode <<= 3;308#ifdef _lib_getgroups309/* on some systems you can be in several groups */310else311{312static int maxgroups;313gid_t *groups=0;314register int n;315if(maxgroups==0)316{317/* first time */318if((maxgroups=getgroups(0,groups)) < 0)319{320/* pre-POSIX system */321maxgroups=NGROUPS_MAX;322}323}324groups = (gid_t*)malloc((maxgroups+1)*sizeof(gid_t));325n = getgroups(maxgroups,groups);326while(--n >= 0)327{328if(groups[n] == statb.st_gid)329{330mode <<= 3;331break;332}333}334}335#endif /* _lib_getgroups */336if(statb.st_mode & mode)337return(0);338}339return(-1);340}341342#ifdef _lib_setreuid343static void setids(int mode,int owner,int group)344{345if(mode & S_ISGID)346setregid(rgroupid,group);347348/* set effective uid even if S_ISUID is not set. This is because349* we are *really* executing EUID root at this point. Even if S_ISUID350* is not set, the value for owner that is passsed should be correct.351*/352setreuid(ruserid,owner);353}354355#else356/*357* This version of setids creats a /tmp file and copies itself into it.358* The "clone" file is made executable with appropriate suid/sgid bits.359* Finally, the clone is exec'ed. This file is unlinked by a grandchild360* of this program, who waits around until the text is free.361*/362363static void setids(int mode,uid_t owner,gid_t group)364{365register int n,m;366int pv[2];367368/*369* Create a token to pass to the new program for validation.370* This token can only be procured by someone running with an371* effective userid of root, and hence gives the clone a way to372* certify that it was really invoked by THISPROG. Someone who373* is already root could spoof us, but why would they want to?374*375* Since we are root here, we must be careful: What if someone376* linked a valuable file to tmpname?377*/378unlink(tmpname); /* should normally fail */379#ifdef O_EXCL380if((n = open(tmpname, O_WRONLY | O_CREAT | O_EXCL, SPECIAL)) < 0 ||381unlink(tmpname) < 0)382#else383if((n = open(tmpname, O_WRONLY | O_CREAT ,SPECIAL)) < 0 || unlink(tmpname) < 0)384#endif385error_exit(badexec);386if(n != FDVERIFY)387{388close(FDVERIFY);389if(fcntl(n,F_DUPFD,FDVERIFY) != FDVERIFY)390error_exit(badexec);391}392mode |= S_IEXEC|(S_IEXEC>>3)|(S_IEXEC>>6);393/* create a pipe for synchronization */394if(pipe(pv) < 0)395error_exit(badexec);396if((n=fork()) == 0)397{ /* child */398close(FDVERIFY);399close(pv[1]);400if((n=fork()) == 0)401{ /* grandchild -- cleans up clone file */402signal(SIGHUP, SIG_IGN);403signal(SIGINT, SIG_IGN);404signal(SIGQUIT, SIG_IGN);405signal(SIGTERM, SIG_IGN);406read(pv[0],pv,1); /* wait for clone to close pipe */407while(unlink(tmpname) < 0 && errno == ETXTBSY)408sleep(1);409exit(0);410}411else if(n == -1)412exit(1);413else414{415/* Create a set[ug]id file that will become the clone.416* To make this atomic, without need for chown(), the417* child takes on desired user and group. The only418* downsize of this that I can see is that it may419* screw up some per- * user accounting.420*/421if((m = open(THISPROG, O_RDONLY)) < 0)422exit(1);423if((mode & S_ISGID) && setgid(group) < 0)424exit(1);425if((mode & S_ISUID) && owner && setuid(owner) < 0)426exit(1);427#ifdef O_EXCL428if((n = open(tmpname,O_WRONLY|O_CREAT|O_TRUNC|O_EXCL, mode)) < 0)429#else430unlink(tmpname);431if((n = open(tmpname,O_WRONLY|O_CREAT|O_TRUNC, mode)) < 0)432#endif /* O_EXCL */433exit(1);434/* populate the clone */435m = mycopy(m,n);436if(chmod(tmpname,mode) <0)437exit(1);438exit(m);439}440}441else if(n == -1)442error_exit(badexec);443else444{445arglist[0] = (char*)tmpname;446close(pv[0]);447/* move write end of pipe into FDSYNC */448if(pv[1] != FDSYNC)449{450close(FDSYNC);451if(fcntl(pv[1],F_DUPFD,FDSYNC) != FDSYNC)452error_exit(badexec);453}454/* wait for child to die */455while((m = wait(0)) != n)456if(m == -1 && errno != EINTR)457break;458/* Kill any setuid status at this point. That way, if the459* clone is not setuid, we won't exec it as root. Also, don't460* neglect to consider that someone could have switched the461* clone file on us.462*/463if(setuid(ruserid) < 0)464error_exit(badexec);465execv(tmpname,arglist);466error_exit(badexec);467}468}469470/*471* create a unique name into the <template>472*/473474static void maketemp(char *template)475{476register char *cp = template;477register pid_t n = getpid();478/* skip to end of string */479while(*++cp);480/* convert process id to string */481while(n > 0)482{483*--cp = (n%10) + '0';484n /= 10;485}486487}488489/*490* copy THISPROG into the open file number <fdo> and close <fdo>491*/492493static int mycopy(int fdi, int fdo)494{495char buffer[BLKSIZE];496register int n;497498while((n = read(fdi,buffer,BLKSIZE)) > 0)499if(write(fdo,buffer,n) != n)500break;501close(fdi);502close(fdo);503return n;504}505506#endif /* _lib_setreuid */507508509510511