xref: /titanic_50/usr/src/lib/libshell/common/sh/suid_exec.c (revision 64cfc8ed0c5da83f827c424588bfe709e3fb5a7a)
1 /***********************************************************************
2 *                                                                      *
3 *               This software is part of the ast package               *
4 *           Copyright (c) 1982-2007 AT&T Knowledge Ventures            *
5 *                      and is licensed under the                       *
6 *                  Common Public License, Version 1.0                  *
7 *                      by AT&T Knowledge Ventures                      *
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 
68 static void error_exit(const char*);
69 static int in_dir(const char*, const char*);
70 static 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 
82 static const char version[]	= "\n@(#)$Id: suid_exec "SH_RELEASE" $\n";
83 static const char badopen[]	= "cannot open";
84 static const char badexec[]	= "cannot exec";
85 static const char devfd[]	= "/dev/fd/10";	/* must match FDIN above */
86 static char tmpname[]		= "/tmp/SUIDXXXXXX";
87 static char **arglist;
88 
89 static char *shell;
90 static char *command;
91 static uid_t ruserid;
92 static uid_t euserid;
93 static gid_t rgroupid;
94 static gid_t egroupid;
95 static struct stat statb;
96 
97 int 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
233 exec:
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 
253 static 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 
271 static 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 
282 static 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 
293 int 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
344 static 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 
364 static 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 
475 static 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 
494 static 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