1/***********************************************************************
2*                                                                      *
3*               This software is part of the ast package               *
4*          Copyright (c) 1982-2011 AT&T Intellectual Property          *
5*                      and is licensed under the                       *
6*                  Common Public License, Version 1.0                  *
7*                    by AT&T Intellectual Property                     *
8*                                                                      *
9*                A copy of the License is available at                 *
10*            http://www.opensource.org/licenses/cpl1.0.txt             *
11*         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         *
12*                                                                      *
13*              Information and Software Systems Research               *
14*                            AT&T Research                             *
15*                           Florham Park NJ                            *
16*                                                                      *
17*                  David Korn <dgk@research.att.com>                   *
18*                                                                      *
19***********************************************************************/
20#pragma prototyped
21/*
22 * This is a program to execute 'execute only' and suid/sgid shell scripts.
23 * This program must be owned by root and must have the set uid bit set.
24 * It must not have the set group id bit set.  This program must be installed
25 * where the define parameter THISPROG indicates to work correctly on system V
26 *
27 *  Written by David Korn
28 *  AT&T Labs
29 *  Enhanced by Rob Stampfli
30 */
31
32/* The file name of the script to execute is argv[0]
33 * Argv[1] is the  program name
34 * The basic idea is to open the script as standard input, set the effective
35 *   user and group id correctly, and then exec the shell.
36 * The complicated part is getting the effective uid of the caller and
37 *   setting the effective uid/gid.  The program which execs this program
38 *   may pass file descriptor FDIN as an open file with mode SPECIAL if
39 *   the effective user id is not the real user id.  The effective
40 *   user id for authentication purposes will be the owner of this
41 *   open file.  On systems without the setreuid() call, e[ug]id is set
42 *   by copying this program to a /tmp/file, making it a suid and/or sgid
43 *   program, and then execing this program.
44 * A forked version of this program waits until it can unlink the /tmp
45 *   file and then exits.  Actually, we fork() twice so the parent can
46 *   wait for the child to complete.  A pipe is used to guarantee that we
47 *   do not remove the /tmp file too soon.
48 */
49
50#include	<ast.h>
51#include	"FEATURE/externs"
52#include	<ls.h>
53#include	<sig.h>
54#include	<error.h>
55#include	<sys/wait.h>
56#include	"version.h"
57
58#define SPECIAL		04100	/* setuid execute only by owner */
59#define FDIN		10	/* must be same as /dev/fd below */
60#undef FDSYNC
61#define FDSYNC		11	/* used on sys5 to synchronize cleanup */
62#define FDVERIFY	12	/* used to validate /tmp process */
63#undef BLKSIZE
64#define BLKSIZE		sizeof(char*)*1024
65#define THISPROG	"/etc/suid_exec"
66#define DEFSHELL	"/bin/sh"
67
68static void error_exit(const char*);
69static int in_dir(const char*, const char*);
70static int endsh(const char*);
71#ifndef _lib_setregid
72#   undef _lib_setreuid
73#endif
74#ifndef _lib_setreuid
75    static void setids(int,uid_t,gid_t);
76    static int mycopy(int, int);
77    static void maketemp(char*);
78#else
79    static void setids(int,int,int);
80#endif /* _lib_setreuid */
81
82static const char version[]	= "\n@(#)$Id: suid_exec "SH_RELEASE" $\n";
83static const char badopen[]	= "cannot open";
84static const char badexec[]	= "cannot exec";
85static const char devfd[]	= "/dev/fd/10";	/* must match FDIN above */
86static char tmpname[]		= "/tmp/SUIDXXXXXX";
87static char **arglist;
88
89static char *shell;
90static char *command;
91static uid_t ruserid;
92static uid_t euserid;
93static gid_t rgroupid;
94static gid_t egroupid;
95static struct stat statb;
96
97int main(int argc,char *argv[])
98{
99	register int m,n;
100	register char *p;
101	struct stat statx;
102	int mode;
103	uid_t effuid;
104	gid_t effgid;
105	NOT_USED(argc);
106	arglist = argv;
107	if((command = argv[1]) == 0)
108		error_exit(badexec);
109	ruserid = getuid();
110	euserid = geteuid();
111	rgroupid = getgid();
112	egroupid = getegid();
113	p = argv[0];
114#ifndef _lib_setreuid
115	maketemp(tmpname);
116	if(strcmp(p,tmpname)==0)
117	{
118		/* At this point, the presumption is that we are the
119		 * version of THISPROG copied into /tmp, with the owner,
120		 * group, and setuid/gid bits correctly set.  This copy of
121		 * the program is executable by anyone, so we must be careful
122		 * not to allow just any invocation of it to succeed, since
123		 * it is setuid/gid.  Validate the proper execution by
124		 * examining the FDVERIFY file descriptor -- if it is owned
125		 * by root and is mode SPECIAL, then this is proof that it was
126		 * passed by a program with superuser privileges -- hence we
127		 * can presume legitimacy.  Otherwise, bail out, as we suspect
128		 * an impostor.
129		 */
130		if(fstat(FDVERIFY,&statb) < 0 || statb.st_uid != 0 ||
131		    (statb.st_mode & ~S_IFMT) != SPECIAL || close(FDVERIFY)<0)
132			error_exit(badexec);
133		/* This enables the grandchild to clean up /tmp file */
134		close(FDSYNC);
135		/* Make sure that this is a valid invocation of the clone.
136		 * Perhaps unnecessary, given FDVERIFY, but what the heck...
137		 */
138		if(stat(tmpname,&statb) < 0 || statb.st_nlink != 1 ||
139		    !S_ISREG(statb.st_mode))
140			error_exit(badexec);
141		if(ruserid != euserid &&
142		  ((statb.st_mode & S_ISUID) == 0 || statb.st_uid != euserid))
143			error_exit(badexec);
144		goto exec;
145	}
146	/* Make sure that this is the real setuid program, not the clone.
147	 * It is possible by clever hacking to get past this point in the
148	 * clone, but it doesn't do the hacker any good that I can see.
149	 */
150	if(euserid)
151		error_exit(badexec);
152#endif /* _lib_setreuid */
153	/* Open the script for reading first and then validate it.  This
154	 * prevents someone from pulling a switcheroo while we are validating.
155	 */
156	n = open(p,0);
157	if(n == FDIN)
158	{
159		n = dup(n);
160		close(FDIN);
161	}
162	if(n < 0)
163		error_exit(badopen);
164	/* validate execution rights to this script */
165	if(fstat(FDIN,&statb) < 0 || (statb.st_mode & ~S_IFMT) != SPECIAL)
166		euserid = ruserid;
167	else
168		euserid = statb.st_uid;
169	/* do it the easy way if you can */
170	if(euserid == ruserid && egroupid == rgroupid)
171	{
172		if(access(p,X_OK) < 0)
173			error_exit(badexec);
174	}
175	else
176	{
177		/* have to check access on each component */
178		while(*p++)
179		{
180			if(*p == '/' || *p == 0)
181			{
182				m = *p;
183				*p = 0;
184				if(eaccess(argv[0],X_OK) < 0)
185					error_exit(badexec);
186				*p = m;
187			}
188		}
189		p = argv[0];
190	}
191	if(fstat(n, &statb) < 0 || !S_ISREG(statb.st_mode))
192		error_exit(badopen);
193	if(stat(p, &statx) < 0 ||
194	  statb.st_ino != statx.st_ino || statb.st_dev != statx.st_dev)
195		error_exit(badexec);
196	if(stat(THISPROG, &statx) < 0 ||
197	  (statb.st_ino == statx.st_ino && statb.st_dev == statx.st_dev))
198		error_exit(badexec);
199	close(FDIN);
200	if(fcntl(n,F_DUPFD,FDIN) != FDIN)
201		error_exit(badexec);
202	close(n);
203
204	/* compute the desired new effective user and group id */
205	effuid = euserid;
206	effgid = egroupid;
207	mode = 0;
208	if(statb.st_mode & S_ISUID)
209		effuid = statb.st_uid;
210	if(statb.st_mode & S_ISGID)
211		effgid = statb.st_gid;
212
213	/* see if group needs setting */
214	if(effgid != egroupid)
215		if(effgid != rgroupid || setgid(rgroupid) < 0)
216			mode = S_ISGID;
217
218	/* now see if the uid needs setting */
219	if(mode)
220	{
221		if(effuid != ruserid)
222			mode |= S_ISUID;
223	}
224	else if(effuid)
225	{
226		if(effuid != ruserid || setuid(ruserid) < 0)
227			mode = S_ISUID;
228	}
229
230	if(mode)
231		setids(mode, effuid, effgid);
232#ifndef _lib_setreuid
233exec:
234#endif /* _lib_setreuid */
235	/* only use SHELL if file is in trusted directory and ends in sh */
236	shell = getenv("SHELL");
237	if(shell == 0 || !endsh(shell) || (
238		!in_dir("/bin",shell) &&
239		!in_dir("/usr/bin",shell) &&
240		!in_dir("/usr/lbin",shell) &&
241		!in_dir("/usr/local/bin",shell)))
242			shell = DEFSHELL;
243	argv[0] = command;
244	argv[1] = (char*)devfd;
245	execv(shell,argv);
246	error_exit(badexec);
247}
248
249/*
250 * return true of shell ends in sh of ksh
251 */
252
253static int endsh(register const char *shell)
254{
255	while(*shell)
256		shell++;
257	if(*--shell != 'h' || *--shell != 's')
258		return(0);
259	if(*--shell=='/')
260		return(1);
261	if(*shell=='k' && *--shell=='/')
262		return(1);
263	return(0);
264}
265
266
267/*
268 * return true of shell is in <dir> directory
269 */
270
271static int in_dir(register const char *dir,register const char *shell)
272{
273	while(*dir)
274	{
275		if(*dir++ != *shell++)
276			return(0);
277	}
278	/* return true if next character is a '/' */
279	return(*shell=='/');
280}
281
282static void error_exit(const char *message)
283{
284	sfprintf(sfstdout,"%s: %s\n",command,message);
285	exit(126);
286}
287
288
289/*
290 * This version of access checks against effective uid and effective gid
291 */
292
293int eaccess(register const char *name, register int mode)
294{
295	struct stat statb;
296	if (stat(name, &statb) == 0)
297	{
298		if(euserid == 0)
299		{
300			if(!S_ISREG(statb.st_mode) || mode != 1)
301				return(0);
302		    	/* root needs execute permission for someone */
303			mode = (S_IXUSR|S_IXGRP|S_IXOTH);
304		}
305		else if(euserid == statb.st_uid)
306			mode <<= 6;
307		else if(egroupid == statb.st_gid)
308			mode <<= 3;
309#ifdef _lib_getgroups
310		/* on some systems you can be in several groups */
311		else
312		{
313			static int maxgroups;
314			gid_t *groups=0;
315			register int n;
316			if(maxgroups==0)
317			{
318				/* first time */
319				if((maxgroups=getgroups(0,groups)) < 0)
320				{
321					/* pre-POSIX system */
322					maxgroups=NGROUPS_MAX;
323				}
324			}
325			groups = (gid_t*)malloc((maxgroups+1)*sizeof(gid_t));
326			n = getgroups(maxgroups,groups);
327			while(--n >= 0)
328			{
329				if(groups[n] == statb.st_gid)
330				{
331					mode <<= 3;
332					break;
333				}
334			}
335		}
336#endif /* _lib_getgroups */
337		if(statb.st_mode & mode)
338			return(0);
339	}
340	return(-1);
341}
342
343#ifdef _lib_setreuid
344static void setids(int mode,int owner,int group)
345{
346	if(mode & S_ISGID)
347		setregid(rgroupid,group);
348
349	/* set effective uid even if S_ISUID is not set.  This is because
350	 * we are *really* executing EUID root at this point.  Even if S_ISUID
351	 * is not set, the value for owner that is passsed should be correct.
352	 */
353	setreuid(ruserid,owner);
354}
355
356#else
357/*
358 * This version of setids creats a /tmp file and copies itself into it.
359 * The "clone" file is made executable with appropriate suid/sgid bits.
360 * Finally, the clone is exec'ed.  This file is unlinked by a grandchild
361 * of this program, who waits around until the text is free.
362 */
363
364static void setids(int mode,uid_t owner,gid_t group)
365{
366	register int n,m;
367	int pv[2];
368
369	/*
370	 * Create a token to pass to the new program for validation.
371	 * This token can only be procured by someone running with an
372	 * effective userid of root, and hence gives the clone a way to
373	 * certify that it was really invoked by THISPROG.  Someone who
374	 * is already root could spoof us, but why would they want to?
375	 *
376	 * Since we are root here, we must be careful:  What if someone
377	 * linked a valuable file to tmpname?
378	 */
379	unlink(tmpname);	/* should normally fail */
380#ifdef O_EXCL
381	if((n = open(tmpname, O_WRONLY | O_CREAT | O_EXCL, SPECIAL)) < 0 ||
382		unlink(tmpname) < 0)
383#else
384	if((n = open(tmpname, O_WRONLY | O_CREAT ,SPECIAL)) < 0 || unlink(tmpname) < 0)
385#endif
386		error_exit(badexec);
387	if(n != FDVERIFY)
388	{
389		close(FDVERIFY);
390		if(fcntl(n,F_DUPFD,FDVERIFY) != FDVERIFY)
391			error_exit(badexec);
392	}
393	mode |= S_IEXEC|(S_IEXEC>>3)|(S_IEXEC>>6);
394	/* create a pipe for synchronization */
395	if(pipe(pv) < 0)
396		error_exit(badexec);
397	if((n=fork()) == 0)
398	{	/* child */
399		close(FDVERIFY);
400		close(pv[1]);
401		if((n=fork()) == 0)
402		{	/* grandchild -- cleans up clone file */
403			signal(SIGHUP, SIG_IGN);
404			signal(SIGINT, SIG_IGN);
405			signal(SIGQUIT, SIG_IGN);
406			signal(SIGTERM, SIG_IGN);
407			read(pv[0],pv,1); /* wait for clone to close pipe */
408			while(unlink(tmpname) < 0 && errno == ETXTBSY)
409				sleep(1);
410			exit(0);
411	    	}
412		else if(n == -1)
413			exit(1);
414		else
415		{
416			/* Create a set[ug]id file that will become the clone.
417			 * To make this atomic, without need for chown(), the
418			 * child takes on desired user and group.  The only
419			 * downsize of this that I can see is that it may
420			 * screw up some per- * user accounting.
421			 */
422			if((m = open(THISPROG, O_RDONLY)) < 0)
423				exit(1);
424			if((mode & S_ISGID) && setgid(group) < 0)
425				exit(1);
426			if((mode & S_ISUID) && owner && setuid(owner) < 0)
427				exit(1);
428#ifdef O_EXCL
429			if((n = open(tmpname,O_WRONLY|O_CREAT|O_TRUNC|O_EXCL, mode)) < 0)
430#else
431			unlink(tmpname);
432			if((n = open(tmpname,O_WRONLY|O_CREAT|O_TRUNC, mode)) < 0)
433#endif /* O_EXCL */
434				exit(1);
435			/* populate the clone */
436			m = mycopy(m,n);
437			if(chmod(tmpname,mode) <0)
438				exit(1);
439			exit(m);
440		}
441	}
442	else if(n == -1)
443		error_exit(badexec);
444	else
445	{
446		arglist[0] = (char*)tmpname;
447		close(pv[0]);
448		/* move write end of pipe into FDSYNC */
449		if(pv[1] != FDSYNC)
450		{
451			close(FDSYNC);
452			if(fcntl(pv[1],F_DUPFD,FDSYNC) != FDSYNC)
453				error_exit(badexec);
454		}
455		/* wait for child to die */
456		while((m = wait(0)) != n)
457			if(m == -1 && errno != EINTR)
458				break;
459		/* Kill any setuid status at this point.  That way, if the
460		 * clone is not setuid, we won't exec it as root.  Also, don't
461		 * neglect to consider that someone could have switched the
462		 * clone file on us.
463		 */
464		if(setuid(ruserid) < 0)
465			error_exit(badexec);
466		execv(tmpname,arglist);
467		error_exit(badexec);
468	}
469}
470
471/*
472 * create a unique name into the <template>
473 */
474
475static void maketemp(char *template)
476{
477	register char *cp = template;
478	register pid_t n = getpid();
479	/* skip to end of string */
480	while(*++cp);
481	/* convert process id to string */
482	while(n > 0)
483	{
484		*--cp = (n%10) + '0';
485		n /= 10;
486	}
487
488}
489
490/*
491 *  copy THISPROG into the open file number <fdo> and close <fdo>
492 */
493
494static int mycopy(int fdi, int fdo)
495{
496	char buffer[BLKSIZE];
497	register int n;
498
499	while((n = read(fdi,buffer,BLKSIZE)) > 0)
500		if(write(fdo,buffer,n) != n)
501			break;
502	close(fdi);
503	close(fdo);
504	return n;
505}
506
507#endif /* _lib_setreuid */
508
509
510