xref: /onnv-gate/usr/src/lib/libshell/common/sh/suid_exec.c (revision 12068:08a39a083754)
14887Schin /***********************************************************************
24887Schin *                                                                      *
34887Schin *               This software is part of the ast package               *
4*12068SRoger.Faulkner@Oracle.COM *          Copyright (c) 1982-2010 AT&T Intellectual Property          *
54887Schin *                      and is licensed under the                       *
64887Schin *                  Common Public License, Version 1.0                  *
78462SApril.Chin@Sun.COM *                    by AT&T Intellectual Property                     *
84887Schin *                                                                      *
94887Schin *                A copy of the License is available at                 *
104887Schin *            http://www.opensource.org/licenses/cpl1.0.txt             *
114887Schin *         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         *
124887Schin *                                                                      *
134887Schin *              Information and Software Systems Research               *
144887Schin *                            AT&T Research                             *
154887Schin *                           Florham Park NJ                            *
164887Schin *                                                                      *
174887Schin *                  David Korn <dgk@research.att.com>                   *
184887Schin *                                                                      *
194887Schin ***********************************************************************/
204887Schin #pragma prototyped
214887Schin /*
224887Schin  * This is a program to execute 'execute only' and suid/sgid shell scripts.
234887Schin  * This program must be owned by root and must have the set uid bit set.
244887Schin  * It must not have the set group id bit set.  This program must be installed
254887Schin  * where the define parameter THISPROG indicates to work correctly on system V
264887Schin  *
274887Schin  *  Written by David Korn
284887Schin  *  AT&T Labs
294887Schin  *  Enhanced by Rob Stampfli
304887Schin  */
314887Schin 
324887Schin /* The file name of the script to execute is argv[0]
334887Schin  * Argv[1] is the  program name
344887Schin  * The basic idea is to open the script as standard input, set the effective
354887Schin  *   user and group id correctly, and then exec the shell.
364887Schin  * The complicated part is getting the effective uid of the caller and
374887Schin  *   setting the effective uid/gid.  The program which execs this program
384887Schin  *   may pass file descriptor FDIN as an open file with mode SPECIAL if
394887Schin  *   the effective user id is not the real user id.  The effective
404887Schin  *   user id for authentication purposes will be the owner of this
414887Schin  *   open file.  On systems without the setreuid() call, e[ug]id is set
424887Schin  *   by copying this program to a /tmp/file, making it a suid and/or sgid
434887Schin  *   program, and then execing this program.
444887Schin  * A forked version of this program waits until it can unlink the /tmp
454887Schin  *   file and then exits.  Actually, we fork() twice so the parent can
464887Schin  *   wait for the child to complete.  A pipe is used to guarantee that we
474887Schin  *   do not remove the /tmp file too soon.
484887Schin  */
494887Schin 
504887Schin #include	<ast.h>
514887Schin #include	"FEATURE/externs"
524887Schin #include	<ls.h>
534887Schin #include	<sig.h>
544887Schin #include	<error.h>
554887Schin #include	<sys/wait.h>
564887Schin #include	"version.h"
574887Schin 
584887Schin #define SPECIAL		04100	/* setuid execute only by owner */
594887Schin #define FDIN		10	/* must be same as /dev/fd below */
604887Schin #undef FDSYNC
614887Schin #define FDSYNC		11	/* used on sys5 to synchronize cleanup */
624887Schin #define FDVERIFY	12	/* used to validate /tmp process */
634887Schin #undef BLKSIZE
644887Schin #define BLKSIZE		sizeof(char*)*1024
654887Schin #define THISPROG	"/etc/suid_exec"
664887Schin #define DEFSHELL	"/bin/sh"
674887Schin 
684887Schin static void error_exit(const char*);
694887Schin static int in_dir(const char*, const char*);
704887Schin static int endsh(const char*);
714887Schin #ifndef _lib_setregid
724887Schin #   undef _lib_setreuid
734887Schin #endif
744887Schin #ifndef _lib_setreuid
754887Schin     static void setids(int,uid_t,gid_t);
764887Schin     static int mycopy(int, int);
774887Schin     static void maketemp(char*);
784887Schin #else
794887Schin     static void setids(int,int,int);
804887Schin #endif /* _lib_setreuid */
814887Schin 
824887Schin static const char version[]	= "\n@(#)$Id: suid_exec "SH_RELEASE" $\n";
834887Schin static const char badopen[]	= "cannot open";
844887Schin static const char badexec[]	= "cannot exec";
854887Schin static const char devfd[]	= "/dev/fd/10";	/* must match FDIN above */
864887Schin static char tmpname[]		= "/tmp/SUIDXXXXXX";
874887Schin static char **arglist;
884887Schin 
894887Schin static char *shell;
904887Schin static char *command;
914887Schin static uid_t ruserid;
924887Schin static uid_t euserid;
934887Schin static gid_t rgroupid;
944887Schin static gid_t egroupid;
954887Schin static struct stat statb;
964887Schin 
main(int argc,char * argv[])974887Schin int main(int argc,char *argv[])
984887Schin {
994887Schin 	register int m,n;
1004887Schin 	register char *p;
1014887Schin 	struct stat statx;
1024887Schin 	int mode;
1034887Schin 	uid_t effuid;
1044887Schin 	gid_t effgid;
1054887Schin 	NOT_USED(argc);
1064887Schin 	arglist = argv;
1074887Schin 	if((command = argv[1]) == 0)
1084887Schin 		error_exit(badexec);
1094887Schin 	ruserid = getuid();
1104887Schin 	euserid = geteuid();
1114887Schin 	rgroupid = getgid();
1124887Schin 	egroupid = getegid();
1134887Schin 	p = argv[0];
1144887Schin #ifndef _lib_setreuid
1154887Schin 	maketemp(tmpname);
1164887Schin 	if(strcmp(p,tmpname)==0)
1174887Schin 	{
1184887Schin 		/* At this point, the presumption is that we are the
1194887Schin 		 * version of THISPROG copied into /tmp, with the owner,
1204887Schin 		 * group, and setuid/gid bits correctly set.  This copy of
1214887Schin 		 * the program is executable by anyone, so we must be careful
1224887Schin 		 * not to allow just any invocation of it to succeed, since
1234887Schin 		 * it is setuid/gid.  Validate the proper execution by
1244887Schin 		 * examining the FDVERIFY file descriptor -- if it is owned
1254887Schin 		 * by root and is mode SPECIAL, then this is proof that it was
1264887Schin 		 * passed by a program with superuser privileges -- hence we
1274887Schin 		 * can presume legitimacy.  Otherwise, bail out, as we suspect
1284887Schin 		 * an impostor.
1294887Schin 		 */
1304887Schin 		if(fstat(FDVERIFY,&statb) < 0 || statb.st_uid != 0 ||
1314887Schin 		    (statb.st_mode & ~S_IFMT) != SPECIAL || close(FDVERIFY)<0)
1324887Schin 			error_exit(badexec);
1334887Schin 		/* This enables the grandchild to clean up /tmp file */
1344887Schin 		close(FDSYNC);
1354887Schin 		/* Make sure that this is a valid invocation of the clone.
1364887Schin 		 * Perhaps unnecessary, given FDVERIFY, but what the heck...
1374887Schin 		 */
1384887Schin 		if(stat(tmpname,&statb) < 0 || statb.st_nlink != 1 ||
1394887Schin 		    !S_ISREG(statb.st_mode))
1404887Schin 			error_exit(badexec);
1414887Schin 		if(ruserid != euserid &&
1424887Schin 		  ((statb.st_mode & S_ISUID) == 0 || statb.st_uid != euserid))
1434887Schin 			error_exit(badexec);
1444887Schin 		goto exec;
1454887Schin 	}
1464887Schin 	/* Make sure that this is the real setuid program, not the clone.
1474887Schin 	 * It is possible by clever hacking to get past this point in the
1484887Schin 	 * clone, but it doesn't do the hacker any good that I can see.
1494887Schin 	 */
1504887Schin 	if(euserid)
1514887Schin 		error_exit(badexec);
1524887Schin #endif /* _lib_setreuid */
1534887Schin 	/* Open the script for reading first and then validate it.  This
1544887Schin 	 * prevents someone from pulling a switcheroo while we are validating.
1554887Schin 	 */
1564887Schin 	n = open(p,0);
1574887Schin 	if(n == FDIN)
1584887Schin 	{
1594887Schin 		n = dup(n);
1604887Schin 		close(FDIN);
1614887Schin 	}
1624887Schin 	if(n < 0)
1634887Schin 		error_exit(badopen);
1644887Schin 	/* validate execution rights to this script */
1654887Schin 	if(fstat(FDIN,&statb) < 0 || (statb.st_mode & ~S_IFMT) != SPECIAL)
1664887Schin 		euserid = ruserid;
1674887Schin 	else
1684887Schin 		euserid = statb.st_uid;
1694887Schin 	/* do it the easy way if you can */
1704887Schin 	if(euserid == ruserid && egroupid == rgroupid)
1714887Schin 	{
1724887Schin 		if(access(p,X_OK) < 0)
1734887Schin 			error_exit(badexec);
1744887Schin 	}
1754887Schin 	else
1764887Schin 	{
1774887Schin 		/* have to check access on each component */
1784887Schin 		while(*p++)
1794887Schin 		{
1804887Schin 			if(*p == '/' || *p == 0)
1814887Schin 			{
1824887Schin 				m = *p;
1834887Schin 				*p = 0;
1844887Schin 				if(eaccess(argv[0],X_OK) < 0)
1854887Schin 					error_exit(badexec);
1864887Schin 				*p = m;
1874887Schin 			}
1884887Schin 		}
1894887Schin 		p = argv[0];
1904887Schin 	}
1914887Schin 	if(fstat(n, &statb) < 0 || !S_ISREG(statb.st_mode))
1924887Schin 		error_exit(badopen);
1934887Schin 	if(stat(p, &statx) < 0 ||
1944887Schin 	  statb.st_ino != statx.st_ino || statb.st_dev != statx.st_dev)
1954887Schin 		error_exit(badexec);
1964887Schin 	if(stat(THISPROG, &statx) < 0 ||
1974887Schin 	  (statb.st_ino == statx.st_ino && statb.st_dev == statx.st_dev))
1984887Schin 		error_exit(badexec);
1994887Schin 	close(FDIN);
2004887Schin 	if(fcntl(n,F_DUPFD,FDIN) != FDIN)
2014887Schin 		error_exit(badexec);
2024887Schin 	close(n);
2034887Schin 
2044887Schin 	/* compute the desired new effective user and group id */
2054887Schin 	effuid = euserid;
2064887Schin 	effgid = egroupid;
2074887Schin 	mode = 0;
2084887Schin 	if(statb.st_mode & S_ISUID)
2094887Schin 		effuid = statb.st_uid;
2104887Schin 	if(statb.st_mode & S_ISGID)
2114887Schin 		effgid = statb.st_gid;
2124887Schin 
2134887Schin 	/* see if group needs setting */
2144887Schin 	if(effgid != egroupid)
2154887Schin 		if(effgid != rgroupid || setgid(rgroupid) < 0)
2164887Schin 			mode = S_ISGID;
2174887Schin 
2184887Schin 	/* now see if the uid needs setting */
2194887Schin 	if(mode)
2204887Schin 	{
2214887Schin 		if(effuid != ruserid)
2224887Schin 			mode |= S_ISUID;
2234887Schin 	}
2244887Schin 	else if(effuid)
2254887Schin 	{
2264887Schin 		if(effuid != ruserid || setuid(ruserid) < 0)
2274887Schin 			mode = S_ISUID;
2284887Schin 	}
2294887Schin 
2304887Schin 	if(mode)
2314887Schin 		setids(mode, effuid, effgid);
2324887Schin #ifndef _lib_setreuid
2334887Schin exec:
2344887Schin #endif /* _lib_setreuid */
2354887Schin 	/* only use SHELL if file is in trusted directory and ends in sh */
2364887Schin 	shell = getenv("SHELL");
2374887Schin 	if(shell == 0 || !endsh(shell) || (
2384887Schin 		!in_dir("/bin",shell) &&
2394887Schin 		!in_dir("/usr/bin",shell) &&
2404887Schin 		!in_dir("/usr/lbin",shell) &&
2414887Schin 		!in_dir("/usr/local/bin",shell)))
2424887Schin 			shell = DEFSHELL;
2434887Schin 	argv[0] = command;
2444887Schin 	argv[1] = (char*)devfd;
2454887Schin 	execv(shell,argv);
2464887Schin 	error_exit(badexec);
2474887Schin }
2484887Schin 
2494887Schin /*
2504887Schin  * return true of shell ends in sh of ksh
2514887Schin  */
2524887Schin 
endsh(register const char * shell)2534887Schin static int endsh(register const char *shell)
2544887Schin {
2554887Schin 	while(*shell)
2564887Schin 		shell++;
2574887Schin 	if(*--shell != 'h' || *--shell != 's')
2584887Schin 		return(0);
2594887Schin 	if(*--shell=='/')
2604887Schin 		return(1);
2614887Schin 	if(*shell=='k' && *--shell=='/')
2624887Schin 		return(1);
2634887Schin 	return(0);
2644887Schin }
2654887Schin 
2664887Schin 
2674887Schin /*
2684887Schin  * return true of shell is in <dir> directory
2694887Schin  */
2704887Schin 
in_dir(register const char * dir,register const char * shell)2714887Schin static int in_dir(register const char *dir,register const char *shell)
2724887Schin {
2734887Schin 	while(*dir)
2744887Schin 	{
2754887Schin 		if(*dir++ != *shell++)
2764887Schin 			return(0);
2774887Schin 	}
2784887Schin 	/* return true if next character is a '/' */
2794887Schin 	return(*shell=='/');
2804887Schin }
2814887Schin 
error_exit(const char * message)2824887Schin static void error_exit(const char *message)
2834887Schin {
2844887Schin 	sfprintf(sfstdout,"%s: %s\n",command,message);
2854887Schin 	exit(126);
2864887Schin }
2874887Schin 
2884887Schin 
2894887Schin /*
2904887Schin  * This version of access checks against effective uid and effective gid
2914887Schin  */
2924887Schin 
eaccess(register const char * name,register int mode)2934887Schin int eaccess(register const char *name, register int mode)
2944887Schin {
2954887Schin 	struct stat statb;
2964887Schin 	if (stat(name, &statb) == 0)
2974887Schin 	{
2984887Schin 		if(euserid == 0)
2994887Schin 		{
3004887Schin 			if(!S_ISREG(statb.st_mode) || mode != 1)
3014887Schin 				return(0);
3024887Schin 		    	/* root needs execute permission for someone */
3034887Schin 			mode = (S_IXUSR|S_IXGRP|S_IXOTH);
3044887Schin 		}
3054887Schin 		else if(euserid == statb.st_uid)
3064887Schin 			mode <<= 6;
3074887Schin 		else if(egroupid == statb.st_gid)
3084887Schin 			mode <<= 3;
3094887Schin #ifdef _lib_getgroups
3104887Schin 		/* on some systems you can be in several groups */
3114887Schin 		else
3124887Schin 		{
3134887Schin 			static int maxgroups;
3144887Schin 			gid_t *groups=0;
3154887Schin 			register int n;
3164887Schin 			if(maxgroups==0)
3174887Schin 			{
3184887Schin 				/* first time */
3194887Schin 				if((maxgroups=getgroups(0,groups)) < 0)
3204887Schin 				{
3214887Schin 					/* pre-POSIX system */
3224887Schin 					maxgroups=NGROUPS_MAX;
3234887Schin 				}
3244887Schin 			}
3254887Schin 			groups = (gid_t*)malloc((maxgroups+1)*sizeof(gid_t));
3264887Schin 			n = getgroups(maxgroups,groups);
3274887Schin 			while(--n >= 0)
3284887Schin 			{
3294887Schin 				if(groups[n] == statb.st_gid)
3304887Schin 				{
3314887Schin 					mode <<= 3;
3324887Schin 					break;
3334887Schin 				}
3344887Schin 			}
3354887Schin 		}
3364887Schin #endif /* _lib_getgroups */
3374887Schin 		if(statb.st_mode & mode)
3384887Schin 			return(0);
3394887Schin 	}
3404887Schin 	return(-1);
3414887Schin }
3424887Schin 
3434887Schin #ifdef _lib_setreuid
setids(int mode,int owner,int group)3444887Schin static void setids(int mode,int owner,int group)
3454887Schin {
3464887Schin 	if(mode & S_ISGID)
3474887Schin 		setregid(rgroupid,group);
3484887Schin 
3494887Schin 	/* set effective uid even if S_ISUID is not set.  This is because
3504887Schin 	 * we are *really* executing EUID root at this point.  Even if S_ISUID
3514887Schin 	 * is not set, the value for owner that is passsed should be correct.
3524887Schin 	 */
3534887Schin 	setreuid(ruserid,owner);
3544887Schin }
3554887Schin 
3564887Schin #else
3574887Schin /*
3584887Schin  * This version of setids creats a /tmp file and copies itself into it.
3594887Schin  * The "clone" file is made executable with appropriate suid/sgid bits.
3604887Schin  * Finally, the clone is exec'ed.  This file is unlinked by a grandchild
3614887Schin  * of this program, who waits around until the text is free.
3624887Schin  */
3634887Schin 
setids(int mode,uid_t owner,gid_t group)3644887Schin static void setids(int mode,uid_t owner,gid_t group)
3654887Schin {
3664887Schin 	register int n,m;
3674887Schin 	int pv[2];
3684887Schin 
3694887Schin 	/*
3704887Schin 	 * Create a token to pass to the new program for validation.
3714887Schin 	 * This token can only be procured by someone running with an
3724887Schin 	 * effective userid of root, and hence gives the clone a way to
3734887Schin 	 * certify that it was really invoked by THISPROG.  Someone who
3744887Schin 	 * is already root could spoof us, but why would they want to?
3754887Schin 	 *
3764887Schin 	 * Since we are root here, we must be careful:  What if someone
3774887Schin 	 * linked a valuable file to tmpname?
3784887Schin 	 */
3794887Schin 	unlink(tmpname);	/* should normally fail */
3804887Schin #ifdef O_EXCL
3814887Schin 	if((n = open(tmpname, O_WRONLY | O_CREAT | O_EXCL, SPECIAL)) < 0 ||
3824887Schin 		unlink(tmpname) < 0)
3834887Schin #else
3844887Schin 	if((n = open(tmpname, O_WRONLY | O_CREAT ,SPECIAL)) < 0 || unlink(tmpname) < 0)
3854887Schin #endif
3864887Schin 		error_exit(badexec);
3874887Schin 	if(n != FDVERIFY)
3884887Schin 	{
3894887Schin 		close(FDVERIFY);
3904887Schin 		if(fcntl(n,F_DUPFD,FDVERIFY) != FDVERIFY)
3914887Schin 			error_exit(badexec);
3924887Schin 	}
3934887Schin 	mode |= S_IEXEC|(S_IEXEC>>3)|(S_IEXEC>>6);
3944887Schin 	/* create a pipe for synchronization */
3954887Schin 	if(pipe(pv) < 0)
3964887Schin 		error_exit(badexec);
3974887Schin 	if((n=fork()) == 0)
3984887Schin 	{	/* child */
3994887Schin 		close(FDVERIFY);
4004887Schin 		close(pv[1]);
4014887Schin 		if((n=fork()) == 0)
4024887Schin 		{	/* grandchild -- cleans up clone file */
4034887Schin 			signal(SIGHUP, SIG_IGN);
4044887Schin 			signal(SIGINT, SIG_IGN);
4054887Schin 			signal(SIGQUIT, SIG_IGN);
4064887Schin 			signal(SIGTERM, SIG_IGN);
4074887Schin 			read(pv[0],pv,1); /* wait for clone to close pipe */
4084887Schin 			while(unlink(tmpname) < 0 && errno == ETXTBSY)
4094887Schin 				sleep(1);
4104887Schin 			exit(0);
4114887Schin 	    	}
4124887Schin 		else if(n == -1)
4134887Schin 			exit(1);
4144887Schin 		else
4154887Schin 		{
4164887Schin 			/* Create a set[ug]id file that will become the clone.
4174887Schin 			 * To make this atomic, without need for chown(), the
4184887Schin 			 * child takes on desired user and group.  The only
4194887Schin 			 * downsize of this that I can see is that it may
4204887Schin 			 * screw up some per- * user accounting.
4214887Schin 			 */
4224887Schin 			if((m = open(THISPROG, O_RDONLY)) < 0)
4234887Schin 				exit(1);
4244887Schin 			if((mode & S_ISGID) && setgid(group) < 0)
4254887Schin 				exit(1);
4264887Schin 			if((mode & S_ISUID) && owner && setuid(owner) < 0)
4274887Schin 				exit(1);
4284887Schin #ifdef O_EXCL
4294887Schin 			if((n = open(tmpname,O_WRONLY|O_CREAT|O_TRUNC|O_EXCL, mode)) < 0)
4304887Schin #else
4314887Schin 			unlink(tmpname);
4324887Schin 			if((n = open(tmpname,O_WRONLY|O_CREAT|O_TRUNC, mode)) < 0)
4334887Schin #endif /* O_EXCL */
4344887Schin 				exit(1);
4354887Schin 			/* populate the clone */
4364887Schin 			m = mycopy(m,n);
4374887Schin 			if(chmod(tmpname,mode) <0)
4384887Schin 				exit(1);
4394887Schin 			exit(m);
4404887Schin 		}
4414887Schin 	}
4424887Schin 	else if(n == -1)
4434887Schin 		error_exit(badexec);
4444887Schin 	else
4454887Schin 	{
4464887Schin 		arglist[0] = (char*)tmpname;
4474887Schin 		close(pv[0]);
4484887Schin 		/* move write end of pipe into FDSYNC */
4494887Schin 		if(pv[1] != FDSYNC)
4504887Schin 		{
4514887Schin 			close(FDSYNC);
4524887Schin 			if(fcntl(pv[1],F_DUPFD,FDSYNC) != FDSYNC)
4534887Schin 				error_exit(badexec);
4544887Schin 		}
4554887Schin 		/* wait for child to die */
4564887Schin 		while((m = wait(0)) != n)
4574887Schin 			if(m == -1 && errno != EINTR)
4584887Schin 				break;
4594887Schin 		/* Kill any setuid status at this point.  That way, if the
4604887Schin 		 * clone is not setuid, we won't exec it as root.  Also, don't
4614887Schin 		 * neglect to consider that someone could have switched the
4624887Schin 		 * clone file on us.
4634887Schin 		 */
4644887Schin 		if(setuid(ruserid) < 0)
4654887Schin 			error_exit(badexec);
4664887Schin 		execv(tmpname,arglist);
4674887Schin 		error_exit(badexec);
4684887Schin 	}
4694887Schin }
4704887Schin 
4714887Schin /*
4724887Schin  * create a unique name into the <template>
4734887Schin  */
4744887Schin 
maketemp(char * template)4754887Schin static void maketemp(char *template)
4764887Schin {
4774887Schin 	register char *cp = template;
4784887Schin 	register pid_t n = getpid();
4794887Schin 	/* skip to end of string */
4804887Schin 	while(*++cp);
4814887Schin 	/* convert process id to string */
4824887Schin 	while(n > 0)
4834887Schin 	{
4844887Schin 		*--cp = (n%10) + '0';
4854887Schin 		n /= 10;
4864887Schin 	}
4874887Schin 
4884887Schin }
4894887Schin 
4904887Schin /*
4914887Schin  *  copy THISPROG into the open file number <fdo> and close <fdo>
4924887Schin  */
4934887Schin 
mycopy(int fdi,int fdo)4944887Schin static int mycopy(int fdi, int fdo)
4954887Schin {
4964887Schin 	char buffer[BLKSIZE];
4974887Schin 	register int n;
4984887Schin 
4994887Schin 	while((n = read(fdi,buffer,BLKSIZE)) > 0)
5004887Schin 		if(write(fdo,buffer,n) != n)
5014887Schin 			break;
5024887Schin 	close(fdi);
5034887Schin 	close(fdo);
5044887Schin 	return n;
5054887Schin }
5064887Schin 
5074887Schin #endif /* _lib_setreuid */
5084887Schin 
5094887Schin 
510