xref: /openbsd-src/usr.bin/doas/doas.c (revision 3374c67d44f9b75b98444cbf63020f777792342e)
1 /* $OpenBSD: doas.c,v 1.98 2022/12/22 19:53:22 kn Exp $ */
2 /*
3  * Copyright (c) 2015 Ted Unangst <tedu@openbsd.org>
4  *
5  * Permission to use, copy, modify, and distribute this software for any
6  * purpose with or without fee is hereby granted, provided that the above
7  * copyright notice and this permission notice appear in all copies.
8  *
9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16  */
17 
18 #include <sys/types.h>
19 #include <sys/stat.h>
20 #include <sys/ioctl.h>
21 
22 #include <limits.h>
23 #include <login_cap.h>
24 #include <bsd_auth.h>
25 #include <readpassphrase.h>
26 #include <string.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <err.h>
30 #include <unistd.h>
31 #include <pwd.h>
32 #include <grp.h>
33 #include <syslog.h>
34 #include <errno.h>
35 #include <fcntl.h>
36 
37 #include "doas.h"
38 
39 static void __dead
40 usage(void)
41 {
42 	fprintf(stderr, "usage: doas [-Lns] [-a style] [-C config] [-u user]"
43 	    " command [arg ...]\n");
44 	exit(1);
45 }
46 
47 static int
48 parseuid(const char *s, uid_t *uid)
49 {
50 	struct passwd *pw;
51 	const char *errstr;
52 
53 	if ((pw = getpwnam(s)) != NULL) {
54 		*uid = pw->pw_uid;
55 		if (*uid == UID_MAX)
56 			return -1;
57 		return 0;
58 	}
59 	*uid = strtonum(s, 0, UID_MAX - 1, &errstr);
60 	if (errstr)
61 		return -1;
62 	return 0;
63 }
64 
65 static int
66 uidcheck(const char *s, uid_t desired)
67 {
68 	uid_t uid;
69 
70 	if (parseuid(s, &uid) != 0)
71 		return -1;
72 	if (uid != desired)
73 		return -1;
74 	return 0;
75 }
76 
77 static int
78 parsegid(const char *s, gid_t *gid)
79 {
80 	struct group *gr;
81 	const char *errstr;
82 
83 	if ((gr = getgrnam(s)) != NULL) {
84 		*gid = gr->gr_gid;
85 		if (*gid == GID_MAX)
86 			return -1;
87 		return 0;
88 	}
89 	*gid = strtonum(s, 0, GID_MAX - 1, &errstr);
90 	if (errstr)
91 		return -1;
92 	return 0;
93 }
94 
95 static int
96 match(uid_t uid, gid_t *groups, int ngroups, uid_t target, const char *cmd,
97     const char **cmdargs, struct rule *r)
98 {
99 	int i;
100 
101 	if (r->ident[0] == ':') {
102 		gid_t rgid;
103 		if (parsegid(r->ident + 1, &rgid) == -1)
104 			return 0;
105 		for (i = 0; i < ngroups; i++) {
106 			if (rgid == groups[i])
107 				break;
108 		}
109 		if (i == ngroups)
110 			return 0;
111 	} else {
112 		if (uidcheck(r->ident, uid) != 0)
113 			return 0;
114 	}
115 	if (r->target && uidcheck(r->target, target) != 0)
116 		return 0;
117 	if (r->cmd) {
118 		if (strcmp(r->cmd, cmd))
119 			return 0;
120 		if (r->cmdargs) {
121 			/* if arguments were given, they should match explicitly */
122 			for (i = 0; r->cmdargs[i]; i++) {
123 				if (!cmdargs[i])
124 					return 0;
125 				if (strcmp(r->cmdargs[i], cmdargs[i]))
126 					return 0;
127 			}
128 			if (cmdargs[i])
129 				return 0;
130 		}
131 	}
132 	return 1;
133 }
134 
135 static int
136 permit(uid_t uid, gid_t *groups, int ngroups, const struct rule **lastr,
137     uid_t target, const char *cmd, const char **cmdargs)
138 {
139 	size_t i;
140 
141 	*lastr = NULL;
142 	for (i = 0; i < nrules; i++) {
143 		if (match(uid, groups, ngroups, target, cmd,
144 		    cmdargs, rules[i]))
145 			*lastr = rules[i];
146 	}
147 	if (!*lastr)
148 		return 0;
149 	return (*lastr)->action == PERMIT;
150 }
151 
152 static void
153 parseconfig(const char *filename, int checkperms)
154 {
155 	extern FILE *yyfp;
156 	extern int yyparse(void);
157 	struct stat sb;
158 
159 	yyfp = fopen(filename, "r");
160 	if (!yyfp)
161 		err(1, checkperms ? "doas is not enabled, %s" :
162 		    "could not open config file %s", filename);
163 
164 	if (checkperms) {
165 		if (fstat(fileno(yyfp), &sb) != 0)
166 			err(1, "fstat(\"%s\")", filename);
167 		if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0)
168 			errx(1, "%s is writable by group or other", filename);
169 		if (sb.st_uid != 0)
170 			errx(1, "%s is not owned by root", filename);
171 	}
172 
173 	yyparse();
174 	fclose(yyfp);
175 	if (parse_error)
176 		exit(1);
177 }
178 
179 static void __dead
180 checkconfig(const char *confpath, int argc, char **argv,
181     uid_t uid, gid_t *groups, int ngroups, uid_t target)
182 {
183 	const struct rule *rule;
184 
185 	setresuid(uid, uid, uid);
186 	if (pledge("stdio rpath getpw", NULL) == -1)
187 		err(1, "pledge");
188 	parseconfig(confpath, 0);
189 	if (!argc)
190 		exit(0);
191 
192 	if (permit(uid, groups, ngroups, &rule, target, argv[0],
193 	    (const char **)argv + 1)) {
194 		printf("permit%s\n", (rule->options & NOPASS) ? " nopass" : "");
195 		exit(0);
196 	} else {
197 		printf("deny\n");
198 		exit(1);
199 	}
200 }
201 
202 static int
203 authuser_checkpass(char *myname, char *login_style)
204 {
205 	char *challenge = NULL, *response, rbuf[1024], cbuf[128];
206 	auth_session_t *as;
207 
208 	if (!(as = auth_userchallenge(myname, login_style, "auth-doas",
209 	    &challenge))) {
210 		warnx("Authentication failed");
211 		return AUTH_FAILED;
212 	}
213 	if (!challenge) {
214 		char host[HOST_NAME_MAX + 1];
215 
216 		if (gethostname(host, sizeof(host)))
217 			snprintf(host, sizeof(host), "?");
218 		snprintf(cbuf, sizeof(cbuf),
219 		    "\rdoas (%.32s@%.32s) password: ", myname, host);
220 		challenge = cbuf;
221 	}
222 	response = readpassphrase(challenge, rbuf, sizeof(rbuf),
223 	    RPP_REQUIRE_TTY);
224 	if (response == NULL && errno == ENOTTY) {
225 		syslog(LOG_AUTHPRIV | LOG_NOTICE,
226 		    "tty required for %s", myname);
227 		errx(1, "a tty is required");
228 	}
229 	if (!auth_userresponse(as, response, 0)) {
230 		explicit_bzero(rbuf, sizeof(rbuf));
231 		syslog(LOG_AUTHPRIV | LOG_NOTICE,
232 		    "failed auth for %s", myname);
233 		warnx("Authentication failed");
234 		return AUTH_FAILED;
235 	}
236 	explicit_bzero(rbuf, sizeof(rbuf));
237 	return AUTH_OK;
238 }
239 
240 static void
241 authuser(char *myname, char *login_style, int persist)
242 {
243 	int i, fd = -1;
244 
245 	if (persist)
246 		fd = open("/dev/tty", O_RDWR);
247 	if (fd != -1) {
248 		if (ioctl(fd, TIOCCHKVERAUTH) == 0)
249 			goto good;
250 	}
251 	for (i = 0; i < AUTH_RETRIES; i++) {
252 		if (authuser_checkpass(myname, login_style) == AUTH_OK)
253 			goto good;
254 	}
255 	exit(1);
256 good:
257 	if (fd != -1) {
258 		int secs = 5 * 60;
259 		ioctl(fd, TIOCSETVERAUTH, &secs);
260 		close(fd);
261 	}
262 }
263 
264 int
265 unveilcommands(const char *ipath, const char *cmd)
266 {
267 	char *path = NULL, *p;
268 	int unveils = 0;
269 
270 	if (strchr(cmd, '/') != NULL) {
271 		if (unveil(cmd, "x") != -1)
272 			unveils++;
273 		goto done;
274 	}
275 
276 	if (!ipath) {
277 		errno = ENOENT;
278 		goto done;
279 	}
280 	path = strdup(ipath);
281 	if (!path) {
282 		errno = ENOENT;
283 		goto done;
284 	}
285 	for (p = path; p && *p; ) {
286 		char buf[PATH_MAX];
287 		char *cp = strsep(&p, ":");
288 
289 		if (cp) {
290 			int r = snprintf(buf, sizeof buf, "%s/%s", cp, cmd);
291 			if (r >= 0 && r < sizeof buf) {
292 				if (unveil(buf, "x") != -1)
293 					unveils++;
294 			}
295 		}
296 	}
297 done:
298 	free(path);
299 	return (unveils);
300 }
301 
302 int
303 main(int argc, char **argv)
304 {
305 	const char *safepath = "/bin:/sbin:/usr/bin:/usr/sbin:"
306 	    "/usr/local/bin:/usr/local/sbin";
307 	const char *confpath = NULL;
308 	char *shargv[] = { NULL, NULL };
309 	char *sh;
310 	const char *p;
311 	const char *cmd;
312 	char cmdline[LINE_MAX];
313 	char mypwbuf[_PW_BUF_LEN], targpwbuf[_PW_BUF_LEN];
314 	struct passwd mypwstore, targpwstore;
315 	struct passwd *mypw, *targpw;
316 	const struct rule *rule;
317 	uid_t uid;
318 	uid_t target = 0;
319 	gid_t groups[NGROUPS_MAX + 1];
320 	int ngroups;
321 	int i, ch, rv;
322 	int sflag = 0;
323 	int nflag = 0;
324 	char cwdpath[PATH_MAX];
325 	const char *cwd;
326 	char *login_style = NULL;
327 	char **envp;
328 
329 	setprogname("doas");
330 
331 	closefrom(STDERR_FILENO + 1);
332 
333 	uid = getuid();
334 
335 	while ((ch = getopt(argc, argv, "a:C:Lnsu:")) != -1) {
336 		switch (ch) {
337 		case 'a':
338 			login_style = optarg;
339 			break;
340 		case 'C':
341 			confpath = optarg;
342 			break;
343 		case 'L':
344 			i = open("/dev/tty", O_RDWR);
345 			if (i != -1)
346 				ioctl(i, TIOCCLRVERAUTH);
347 			exit(i == -1);
348 		case 'u':
349 			if (parseuid(optarg, &target) != 0)
350 				errx(1, "unknown user");
351 			break;
352 		case 'n':
353 			nflag = 1;
354 			break;
355 		case 's':
356 			sflag = 1;
357 			break;
358 		default:
359 			usage();
360 			break;
361 		}
362 	}
363 	argv += optind;
364 	argc -= optind;
365 
366 	if (confpath) {
367 		if (sflag)
368 			usage();
369 	} else if ((!sflag && !argc) || (sflag && argc))
370 		usage();
371 
372 	rv = getpwuid_r(uid, &mypwstore, mypwbuf, sizeof(mypwbuf), &mypw);
373 	if (rv != 0)
374 		err(1, "getpwuid_r failed");
375 	if (mypw == NULL)
376 		errx(1, "no passwd entry for self");
377 	ngroups = getgroups(NGROUPS_MAX, groups);
378 	if (ngroups == -1)
379 		err(1, "can't get groups");
380 	groups[ngroups++] = getgid();
381 
382 	if (sflag) {
383 		sh = getenv("SHELL");
384 		if (sh == NULL || *sh == '\0') {
385 			shargv[0] = mypw->pw_shell;
386 		} else
387 			shargv[0] = sh;
388 		argv = shargv;
389 		argc = 1;
390 	}
391 
392 	if (confpath) {
393 		if (pledge("stdio rpath getpw id", NULL) == -1)
394 			err(1, "pledge");
395 		checkconfig(confpath, argc, argv, uid, groups, ngroups,
396 		    target);
397 		exit(1);	/* fail safe */
398 	}
399 
400 	if (geteuid())
401 		errx(1, "not installed setuid");
402 
403 	parseconfig("/etc/doas.conf", 1);
404 
405 	/* cmdline is used only for logging, no need to abort on truncate */
406 	(void)strlcpy(cmdline, argv[0], sizeof(cmdline));
407 	for (i = 1; i < argc; i++) {
408 		if (strlcat(cmdline, " ", sizeof(cmdline)) >= sizeof(cmdline))
409 			break;
410 		if (strlcat(cmdline, argv[i], sizeof(cmdline)) >= sizeof(cmdline))
411 			break;
412 	}
413 
414 	cmd = argv[0];
415 	if (!permit(uid, groups, ngroups, &rule, target, cmd,
416 	    (const char **)argv + 1)) {
417 		syslog(LOG_AUTHPRIV | LOG_NOTICE,
418 		    "command not permitted for %s: %s", mypw->pw_name, cmdline);
419 		errc(1, EPERM, NULL);
420 	}
421 
422 	if (!(rule->options & NOPASS)) {
423 		if (nflag)
424 			errx(1, "Authentication required");
425 
426 		authuser(mypw->pw_name, login_style, rule->options & PERSIST);
427 	}
428 
429 	if ((p = getenv("PATH")) != NULL)
430 		formerpath = strdup(p);
431 	if (formerpath == NULL)
432 		formerpath = "";
433 
434 	if (unveil(_PATH_LOGIN_CONF, "r") == -1)
435 		err(1, "unveil %s", _PATH_LOGIN_CONF);
436 	if (unveil(_PATH_LOGIN_CONF ".db", "r") == -1)
437 		err(1, "unveil %s.db", _PATH_LOGIN_CONF);
438 	if (unveil(_PATH_LOGIN_CONF_D, "r") == -1)
439 		err(1, "unveil %s", _PATH_LOGIN_CONF_D);
440 	if (rule->cmd) {
441 		if (setenv("PATH", safepath, 1) == -1)
442 			err(1, "failed to set PATH '%s'", safepath);
443 	}
444 	if (unveilcommands(getenv("PATH"), cmd) == 0)
445 		goto fail;
446 
447 	if (pledge("stdio rpath getpw exec id", NULL) == -1)
448 		err(1, "pledge");
449 
450 	rv = getpwuid_r(target, &targpwstore, targpwbuf, sizeof(targpwbuf), &targpw);
451 	if (rv != 0)
452 		err(1, "getpwuid_r failed");
453 	if (targpw == NULL)
454 		errx(1, "no passwd entry for target");
455 
456 	if (setusercontext(NULL, targpw, target, LOGIN_SETGROUP |
457 	    LOGIN_SETPATH |
458 	    LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK |
459 	    LOGIN_SETUSER | LOGIN_SETENV | LOGIN_SETRTABLE) != 0)
460 		errx(1, "failed to set user context for target");
461 
462 	if (pledge("stdio rpath exec", NULL) == -1)
463 		err(1, "pledge");
464 
465 	if (getcwd(cwdpath, sizeof(cwdpath)) == NULL)
466 		cwd = "(failed)";
467 	else
468 		cwd = cwdpath;
469 
470 	if (pledge("stdio exec", NULL) == -1)
471 		err(1, "pledge");
472 
473 	if (!(rule->options & NOLOG)) {
474 		syslog(LOG_AUTHPRIV | LOG_INFO,
475 		    "%s ran command %s as %s from %s",
476 		    mypw->pw_name, cmdline, targpw->pw_name, cwd);
477 	}
478 
479 	envp = prepenv(rule, mypw, targpw);
480 
481 	/* setusercontext set path for the next process, so reset it for us */
482 	if (rule->cmd) {
483 		if (setenv("PATH", safepath, 1) == -1)
484 			err(1, "failed to set PATH '%s'", safepath);
485 	} else {
486 		if (setenv("PATH", formerpath, 1) == -1)
487 			err(1, "failed to set PATH '%s'", formerpath);
488 	}
489 	execvpe(cmd, argv, envp);
490 fail:
491 	if (errno == ENOENT)
492 		errx(1, "%s: command not found", cmd);
493 	err(1, "%s", cmd);
494 }
495