1 /***********************************************************************
2 * *
3 * This software is part of the ast package *
4 * Copyright (c) 1982-2010 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
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
main(int argc,char * argv[])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
endsh(register const char * shell)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
in_dir(register const char * dir,register const char * shell)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
error_exit(const char * message)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
eaccess(register const char * name,register int mode)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
setids(int mode,int owner,int group)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
setids(int mode,uid_t owner,gid_t group)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
maketemp(char * template)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
mycopy(int fdi,int fdo)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