1 /*********************************************************************** 2 * * 3 * This software is part of the ast package * 4 * Copyright (c) 1982-2008 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 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