xref: /freebsd/usr.bin/su/su.c (revision 74bf4e164ba5851606a27d4feff27717452583e5)
1 /*
2  * Copyright (c) 1988, 1993, 1994
3  *	The Regents of the University of California.  All rights reserved.
4  * Copyright (c) 2002 Networks Associates Technologies, Inc.
5  * All rights reserved.
6  *
7  * Portions of this software were developed for the FreeBSD Project by
8  * ThinkSec AS and NAI Labs, the Security Research Division of Network
9  * Associates, Inc.  under DARPA/SPAWAR contract N66001-01-C-8035
10  * ("CBOSS"), as part of the DARPA CHATS research program.
11  *
12  * Redistribution and use in source and binary forms, with or without
13  * modification, are permitted provided that the following conditions
14  * are met:
15  * 1. Redistributions of source code must retain the above copyright
16  *    notice, this list of conditions and the following disclaimer.
17  * 2. Redistributions in binary form must reproduce the above copyright
18  *    notice, this list of conditions and the following disclaimer in the
19  *    documentation and/or other materials provided with the distribution.
20  * 3. All advertising materials mentioning features or use of this software
21  *    must display the following acknowledgement:
22  *	This product includes software developed by the University of
23  *	California, Berkeley and its contributors.
24  * 4. Neither the name of the University nor the names of its contributors
25  *    may be used to endorse or promote products derived from this software
26  *    without specific prior written permission.
27  *
28  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
29  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
31  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
32  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
34  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
35  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
36  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
37  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
38  * SUCH DAMAGE.
39  */
40 
41 #ifndef lint
42 static const char copyright[] =
43 "@(#) Copyright (c) 1988, 1993, 1994\n\
44 	The Regents of the University of California.  All rights reserved.\n";
45 #endif /* not lint */
46 
47 #if 0
48 #ifndef lint
49 static char sccsid[] = "@(#)su.c	8.3 (Berkeley) 4/2/94";
50 #endif /* not lint */
51 #endif
52 
53 #include <sys/cdefs.h>
54 __FBSDID("$FreeBSD$");
55 
56 #include <sys/param.h>
57 #include <sys/time.h>
58 #include <sys/resource.h>
59 #include <sys/wait.h>
60 
61 #include <err.h>
62 #include <errno.h>
63 #include <grp.h>
64 #include <login_cap.h>
65 #include <paths.h>
66 #include <pwd.h>
67 #include <signal.h>
68 #include <stdio.h>
69 #include <stdlib.h>
70 #include <string.h>
71 #include <syslog.h>
72 #include <unistd.h>
73 
74 #include <security/pam_appl.h>
75 #include <security/openpam.h>
76 
77 #define PAM_END() do {							\
78 	int local_ret;							\
79 	if (pamh != NULL) {						\
80 		local_ret = pam_setcred(pamh, PAM_DELETE_CRED);		\
81 		if (local_ret != PAM_SUCCESS)				\
82 			syslog(LOG_ERR, "pam_setcred: %s",		\
83 				pam_strerror(pamh, local_ret));		\
84 		if (asthem) {						\
85 			local_ret = pam_close_session(pamh, 0);		\
86 			if (local_ret != PAM_SUCCESS)			\
87 				syslog(LOG_ERR, "pam_close_session: %s",\
88 					pam_strerror(pamh, local_ret));	\
89 		}							\
90 		local_ret = pam_end(pamh, local_ret);			\
91 		if (local_ret != PAM_SUCCESS)				\
92 			syslog(LOG_ERR, "pam_end: %s",			\
93 				pam_strerror(pamh, local_ret));		\
94 	}								\
95 } while (0)
96 
97 
98 #define PAM_SET_ITEM(what, item) do {					\
99 	int local_ret;							\
100 	local_ret = pam_set_item(pamh, what, item);			\
101 	if (local_ret != PAM_SUCCESS) {					\
102 		syslog(LOG_ERR, "pam_set_item(" #what "): %s",		\
103 			pam_strerror(pamh, local_ret));			\
104 		errx(1, "pam_set_item(" #what "): %s",			\
105 			pam_strerror(pamh, local_ret));			\
106 		/* NOTREACHED */					\
107 	}								\
108 } while (0)
109 
110 enum tristate { UNSET, YES, NO };
111 
112 static pam_handle_t *pamh = NULL;
113 static char	**environ_pam;
114 
115 static char	*ontty(void);
116 static int	chshell(const char *);
117 static void	usage(void) __dead2;
118 static void	export_pam_environment(void);
119 static int	ok_to_export(const char *);
120 
121 extern char	**environ;
122 
123 int
124 main(int argc, char *argv[])
125 {
126 	static char	*cleanenv;
127 	struct passwd	*pwd;
128 	struct pam_conv	conv = { openpam_ttyconv, NULL };
129 	enum tristate	iscsh;
130 	login_cap_t	*lc;
131 	union {
132 		const char	**a;
133 		char		* const *b;
134 	}		np;
135 	uid_t		ruid;
136 	pid_t		child_pid, child_pgrp, pid;
137 	int		asme, ch, asthem, fastlogin, prio, i, retcode,
138 			statusp, setmaclabel;
139 	u_int		setwhat;
140 	char		*username, *class, shellbuf[MAXPATHLEN];
141 	const char	*p, *user, *shell, *mytty, **nargv;
142 	struct sigaction sa, sa_int, sa_quit, sa_pipe;
143 	int temp, fds[2];
144 
145 	shell = class = cleanenv = NULL;
146 	asme = asthem = fastlogin = statusp = 0;
147 	user = "root";
148 	iscsh = UNSET;
149 	setmaclabel = 0;
150 
151 	while ((ch = getopt(argc, argv, "-flmsc:")) != -1)
152 		switch ((char)ch) {
153 		case 'f':
154 			fastlogin = 1;
155 			break;
156 		case '-':
157 		case 'l':
158 			asme = 0;
159 			asthem = 1;
160 			break;
161 		case 'm':
162 			asme = 1;
163 			asthem = 0;
164 			break;
165 		case 's':
166 			setmaclabel = 1;
167 			break;
168 		case 'c':
169 			class = optarg;
170 			break;
171 		case '?':
172 		default:
173 			usage();
174 		/* NOTREACHED */
175 		}
176 
177 	if (optind < argc)
178 		user = argv[optind++];
179 
180 	if (user == NULL)
181 		usage();
182 	/* NOTREACHED */
183 
184 	if (strlen(user) > MAXLOGNAME - 1)
185 		errx(1, "username too long");
186 
187 	nargv = malloc(sizeof(char *) * (size_t)(argc + 4));
188 	if (nargv == NULL)
189 		errx(1, "malloc failure");
190 
191 	nargv[argc + 3] = NULL;
192 	for (i = argc; i >= optind; i--)
193 		nargv[i + 3] = argv[i];
194 	np.a = &nargv[i + 3];
195 
196 	argv += optind;
197 
198 	errno = 0;
199 	prio = getpriority(PRIO_PROCESS, 0);
200 	if (errno)
201 		prio = 0;
202 
203 	setpriority(PRIO_PROCESS, 0, -2);
204 	openlog("su", LOG_CONS, LOG_AUTH);
205 
206 	/* get current login name, real uid and shell */
207 	ruid = getuid();
208 	username = getlogin();
209 	pwd = getpwnam(username);
210 	if (username == NULL || pwd == NULL || pwd->pw_uid != ruid)
211 		pwd = getpwuid(ruid);
212 	if (pwd == NULL)
213 		errx(1, "who are you?");
214 
215 	username = strdup(pwd->pw_name);
216 	if (username == NULL)
217 		err(1, "strdup failure");
218 
219 	if (asme) {
220 		if (pwd->pw_shell != NULL && *pwd->pw_shell != '\0') {
221 			/* must copy - pwd memory is recycled */
222 			shell = strncpy(shellbuf, pwd->pw_shell,
223 			    sizeof(shellbuf));
224 			shellbuf[sizeof(shellbuf) - 1] = '\0';
225 		}
226 		else {
227 			shell = _PATH_BSHELL;
228 			iscsh = NO;
229 		}
230 	}
231 
232 	/* Do the whole PAM startup thing */
233 	retcode = pam_start("su", user, &conv, &pamh);
234 	if (retcode != PAM_SUCCESS) {
235 		syslog(LOG_ERR, "pam_start: %s", pam_strerror(pamh, retcode));
236 		errx(1, "pam_start: %s", pam_strerror(pamh, retcode));
237 	}
238 
239 	PAM_SET_ITEM(PAM_RUSER, username);
240 
241 	mytty = ttyname(STDERR_FILENO);
242 	if (!mytty)
243 		mytty = "tty";
244 	PAM_SET_ITEM(PAM_TTY, mytty);
245 
246 	retcode = pam_authenticate(pamh, 0);
247 	if (retcode != PAM_SUCCESS) {
248 		syslog(LOG_AUTH|LOG_WARNING, "BAD SU %s to %s on %s",
249 		    username, user, mytty);
250 		errx(1, "Sorry");
251 	}
252 	retcode = pam_get_item(pamh, PAM_USER, (const void **)&p);
253 	if (retcode == PAM_SUCCESS)
254 		user = p;
255 	else
256 		syslog(LOG_ERR, "pam_get_item(PAM_USER): %s",
257 		    pam_strerror(pamh, retcode));
258 	pwd = getpwnam(user);
259 	if (pwd == NULL)
260 		errx(1, "unknown login: %s", user);
261 
262 	retcode = pam_acct_mgmt(pamh, 0);
263 	if (retcode == PAM_NEW_AUTHTOK_REQD) {
264 		retcode = pam_chauthtok(pamh,
265 			PAM_CHANGE_EXPIRED_AUTHTOK);
266 		if (retcode != PAM_SUCCESS) {
267 			syslog(LOG_ERR, "pam_chauthtok: %s",
268 			    pam_strerror(pamh, retcode));
269 			errx(1, "Sorry");
270 		}
271 	}
272 	if (retcode != PAM_SUCCESS) {
273 		syslog(LOG_ERR, "pam_acct_mgmt: %s",
274 			pam_strerror(pamh, retcode));
275 		errx(1, "Sorry");
276 	}
277 
278 	/* get target login information */
279 	if (class == NULL)
280 		lc = login_getpwclass(pwd);
281 	else {
282 		if (ruid != 0)
283 			errx(1, "only root may use -c");
284 		lc = login_getclass(class);
285 		if (lc == NULL)
286 			errx(1, "unknown class: %s", class);
287 	}
288 
289 	/* if asme and non-standard target shell, must be root */
290 	if (asme) {
291 		if (ruid != 0 && !chshell(pwd->pw_shell))
292 			errx(1, "permission denied (shell)");
293 	}
294 	else if (pwd->pw_shell && *pwd->pw_shell) {
295 		shell = pwd->pw_shell;
296 		iscsh = UNSET;
297 	}
298 	else {
299 		shell = _PATH_BSHELL;
300 		iscsh = NO;
301 	}
302 
303 	/* if we're forking a csh, we want to slightly muck the args */
304 	if (iscsh == UNSET) {
305 		p = strrchr(shell, '/');
306 		if (p)
307 			++p;
308 		else
309 			p = shell;
310 		iscsh = strcmp(p, "csh") ? (strcmp(p, "tcsh") ? NO : YES) : YES;
311 	}
312 	setpriority(PRIO_PROCESS, 0, prio);
313 
314 	/* Switch to home directory */
315 	if (asthem) {
316 		if (chdir(pwd->pw_dir) < 0)
317 			errx(1, "no directory");
318 	}
319 
320 	/*
321 	 * PAM modules might add supplementary groups in pam_setcred(), so
322 	 * initialize them first.
323 	 */
324 	if (setusercontext(lc, pwd, pwd->pw_uid, LOGIN_SETGROUP) < 0)
325 		err(1, "setusercontext");
326 
327 	retcode = pam_setcred(pamh, PAM_ESTABLISH_CRED);
328 	if (retcode != PAM_SUCCESS) {
329 		syslog(LOG_ERR, "pam_setcred: %s",
330 		    pam_strerror(pamh, retcode));
331 		errx(1, "failed to establish credentials.");
332 	}
333 	if (asthem) {
334 		retcode = pam_open_session(pamh, 0);
335 		if (retcode != PAM_SUCCESS) {
336 			syslog(LOG_ERR, "pam_open_session: %s",
337 			    pam_strerror(pamh, retcode));
338 			errx(1, "failed to open session.");
339 		}
340 	}
341 
342 	/*
343 	 * We must fork() before setuid() because we need to call
344 	 * pam_setcred(pamh, PAM_DELETE_CRED) as root.
345 	 */
346 	sa.sa_flags = SA_RESTART;
347 	sa.sa_handler = SIG_IGN;
348 	sigemptyset(&sa.sa_mask);
349 	sigaction(SIGINT, &sa, &sa_int);
350 	sigaction(SIGQUIT, &sa, &sa_quit);
351 	sigaction(SIGPIPE, &sa, &sa_pipe);
352 	sa.sa_handler = SIG_DFL;
353 	sigaction(SIGTSTP, &sa, NULL);
354 	statusp = 1;
355 	if (pipe(fds) == -1) {
356 		PAM_END();
357 		err(1, "pipe");
358 	}
359 	child_pid = fork();
360 	switch (child_pid) {
361 	default:
362 		sa.sa_handler = SIG_IGN;
363 		sigaction(SIGTTOU, &sa, NULL);
364 		close(fds[0]);
365 		setpgid(child_pid, child_pid);
366 		tcsetpgrp(STDERR_FILENO, child_pid);
367 		close(fds[1]);
368 		sigaction(SIGPIPE, &sa_pipe, NULL);
369 		while ((pid = waitpid(child_pid, &statusp, WUNTRACED)) != -1) {
370 			if (WIFSTOPPED(statusp)) {
371 				kill(getpid(), SIGSTOP);
372 				child_pgrp = getpgid(child_pid);
373 				tcsetpgrp(1, child_pgrp);
374 				kill(child_pid, SIGCONT);
375 				statusp = 1;
376 				continue;
377 			}
378 			break;
379 		}
380 		tcsetpgrp(STDERR_FILENO, getpgrp());
381 		if (pid == -1)
382 			err(1, "waitpid");
383 		PAM_END();
384 		exit(WEXITSTATUS(statusp));
385 	case -1:
386 		PAM_END();
387 		err(1, "fork");
388 	case 0:
389 		close(fds[1]);
390 		read(fds[0], &temp, 1);
391 		close(fds[0]);
392 		sigaction(SIGPIPE, &sa_pipe, NULL);
393 		sigaction(SIGINT, &sa_int, NULL);
394 		sigaction(SIGQUIT, &sa_quit, NULL);
395 
396 		/*
397 		 * Set all user context except for: Environmental variables
398 		 * Umask Login records (wtmp, etc) Path
399 		 */
400 		setwhat = LOGIN_SETALL & ~(LOGIN_SETENV | LOGIN_SETUMASK |
401 			   LOGIN_SETLOGIN | LOGIN_SETPATH | LOGIN_SETGROUP |
402 			   LOGIN_SETMAC);
403 		/*
404 		 * If -s is present, also set the MAC label.
405 		 */
406 		if (setmaclabel)
407 			setwhat |= LOGIN_SETMAC;
408 		/*
409 		 * Don't touch resource/priority settings if -m has been used
410 		 * or -l and -c hasn't, and we're not su'ing to root.
411 		 */
412 		if ((asme || (!asthem && class == NULL)) && pwd->pw_uid)
413 			setwhat &= ~(LOGIN_SETPRIORITY | LOGIN_SETRESOURCES);
414 		if (setusercontext(lc, pwd, pwd->pw_uid, setwhat) < 0)
415 			err(1, "setusercontext");
416 
417 		if (!asme) {
418 			if (asthem) {
419 				p = getenv("TERM");
420 				environ = &cleanenv;
421 			}
422 
423 			if (asthem || pwd->pw_uid)
424 				setenv("USER", pwd->pw_name, 1);
425 			setenv("HOME", pwd->pw_dir, 1);
426 			setenv("SHELL", shell, 1);
427 
428 			if (asthem) {
429 				/*
430 				 * Add any environmental variables that the
431 				 * PAM modules may have set.
432 				 */
433 				environ_pam = pam_getenvlist(pamh);
434 				if (environ_pam)
435 					export_pam_environment();
436 
437 				/* set the su'd user's environment & umask */
438 				setusercontext(lc, pwd, pwd->pw_uid,
439 					LOGIN_SETPATH | LOGIN_SETUMASK |
440 					LOGIN_SETENV);
441 				if (p)
442 					setenv("TERM", p, 1);
443 			}
444 		}
445 		login_close(lc);
446 
447 		if (iscsh == YES) {
448 			if (fastlogin)
449 				*np.a-- = "-f";
450 			if (asme)
451 				*np.a-- = "-m";
452 		}
453 		/* csh strips the first character... */
454 		*np.a = asthem ? "-su" : iscsh == YES ? "_su" : "su";
455 
456 		if (ruid != 0)
457 			syslog(LOG_NOTICE, "%s to %s%s", username, user,
458 			    ontty());
459 
460 		execv(shell, np.b);
461 		err(1, "%s", shell);
462 	}
463 }
464 
465 static void
466 export_pam_environment(void)
467 {
468 	char	**pp;
469 
470 	for (pp = environ_pam; *pp != NULL; pp++) {
471 		if (ok_to_export(*pp))
472 			putenv(*pp);
473 		free(*pp);
474 	}
475 }
476 
477 /*
478  * Sanity checks on PAM environmental variables:
479  * - Make sure there is an '=' in the string.
480  * - Make sure the string doesn't run on too long.
481  * - Do not export certain variables.  This list was taken from the
482  *   Solaris pam_putenv(3) man page.
483  * Note that if the user is chrooted, PAM may have a better idea than we
484  * do of where her home directory is.
485  */
486 static int
487 ok_to_export(const char *s)
488 {
489 	static const char *noexport[] = {
490 		"SHELL", /* "HOME", */ "LOGNAME", "MAIL", "CDPATH",
491 		"IFS", "PATH", NULL
492 	};
493 	const char **pp;
494 	size_t n;
495 
496 	if (strlen(s) > 1024 || strchr(s, '=') == NULL)
497 		return 0;
498 	if (strncmp(s, "LD_", 3) == 0)
499 		return 0;
500 	for (pp = noexport; *pp != NULL; pp++) {
501 		n = strlen(*pp);
502 		if (s[n] == '=' && strncmp(s, *pp, n) == 0)
503 			return 0;
504 	}
505 	return 1;
506 }
507 
508 static void
509 usage(void)
510 {
511 
512 	fprintf(stderr, "usage: su [-] [-flms] [-c class] [login [args]]\n");
513 	exit(1);
514 	/* NOTREACHED */
515 }
516 
517 static int
518 chshell(const char *sh)
519 {
520 	int r;
521 	char *cp;
522 
523 	r = 0;
524 	setusershell();
525 	while ((cp = getusershell()) != NULL && !r)
526 	    r = (strcmp(cp, sh) == 0);
527 	endusershell();
528 	return r;
529 }
530 
531 static char *
532 ontty(void)
533 {
534 	char *p;
535 	static char buf[MAXPATHLEN + 4];
536 
537 	buf[0] = 0;
538 	p = ttyname(STDERR_FILENO);
539 	if (p)
540 		snprintf(buf, sizeof(buf), " on %s", p);
541 	return buf;
542 }
543