xref: /openbsd/usr.bin/doas/doas.c (revision ee4ffdb6)
1 /* $OpenBSD: doas.c,v 1.78 2019/06/17 19:51:23 tedu 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 [args]\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 		return 0;
56 	}
57 	*uid = strtonum(s, 0, UID_MAX, &errstr);
58 	if (errstr)
59 		return -1;
60 	return 0;
61 }
62 
63 static int
64 uidcheck(const char *s, uid_t desired)
65 {
66 	uid_t uid;
67 
68 	if (parseuid(s, &uid) != 0)
69 		return -1;
70 	if (uid != desired)
71 		return -1;
72 	return 0;
73 }
74 
75 static int
76 parsegid(const char *s, gid_t *gid)
77 {
78 	struct group *gr;
79 	const char *errstr;
80 
81 	if ((gr = getgrnam(s)) != NULL) {
82 		*gid = gr->gr_gid;
83 		return 0;
84 	}
85 	*gid = strtonum(s, 0, GID_MAX, &errstr);
86 	if (errstr)
87 		return -1;
88 	return 0;
89 }
90 
91 static int
92 match(uid_t uid, gid_t *groups, int ngroups, uid_t target, const char *cmd,
93     const char **cmdargs, struct rule *r)
94 {
95 	int i;
96 
97 	if (r->ident[0] == ':') {
98 		gid_t rgid;
99 		if (parsegid(r->ident + 1, &rgid) == -1)
100 			return 0;
101 		for (i = 0; i < ngroups; i++) {
102 			if (rgid == groups[i])
103 				break;
104 		}
105 		if (i == ngroups)
106 			return 0;
107 	} else {
108 		if (uidcheck(r->ident, uid) != 0)
109 			return 0;
110 	}
111 	if (r->target && uidcheck(r->target, target) != 0)
112 		return 0;
113 	if (r->cmd) {
114 		if (strcmp(r->cmd, cmd))
115 			return 0;
116 		if (r->cmdargs) {
117 			/* if arguments were given, they should match explicitly */
118 			for (i = 0; r->cmdargs[i]; i++) {
119 				if (!cmdargs[i])
120 					return 0;
121 				if (strcmp(r->cmdargs[i], cmdargs[i]))
122 					return 0;
123 			}
124 			if (cmdargs[i])
125 				return 0;
126 		}
127 	}
128 	return 1;
129 }
130 
131 static int
132 permit(uid_t uid, gid_t *groups, int ngroups, const struct rule **lastr,
133     uid_t target, const char *cmd, const char **cmdargs)
134 {
135 	int i;
136 
137 	*lastr = NULL;
138 	for (i = 0; i < nrules; i++) {
139 		if (match(uid, groups, ngroups, target, cmd,
140 		    cmdargs, rules[i]))
141 			*lastr = rules[i];
142 	}
143 	if (!*lastr)
144 		return 0;
145 	return (*lastr)->action == PERMIT;
146 }
147 
148 static void
149 parseconfig(const char *filename, int checkperms)
150 {
151 	extern FILE *yyfp;
152 	extern int yyparse(void);
153 	struct stat sb;
154 
155 	yyfp = fopen(filename, "r");
156 	if (!yyfp)
157 		err(1, checkperms ? "doas is not enabled, %s" :
158 		    "could not open config file %s", filename);
159 
160 	if (checkperms) {
161 		if (fstat(fileno(yyfp), &sb) != 0)
162 			err(1, "fstat(\"%s\")", filename);
163 		if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0)
164 			errx(1, "%s is writable by group or other", filename);
165 		if (sb.st_uid != 0)
166 			errx(1, "%s is not owned by root", filename);
167 	}
168 
169 	yyparse();
170 	fclose(yyfp);
171 	if (parse_errors)
172 		exit(1);
173 }
174 
175 static void __dead
176 checkconfig(const char *confpath, int argc, char **argv,
177     uid_t uid, gid_t *groups, int ngroups, uid_t target)
178 {
179 	const struct rule *rule;
180 
181 	setresuid(uid, uid, uid);
182 	parseconfig(confpath, 0);
183 	if (!argc)
184 		exit(0);
185 
186 	if (permit(uid, groups, ngroups, &rule, target, argv[0],
187 	    (const char **)argv + 1)) {
188 		printf("permit%s\n", (rule->options & NOPASS) ? " nopass" : "");
189 		exit(0);
190 	} else {
191 		printf("deny\n");
192 		exit(1);
193 	}
194 }
195 
196 static void
197 authuser(char *myname, char *login_style, int persist)
198 {
199 	char *challenge = NULL, *response, rbuf[1024], cbuf[128];
200 	auth_session_t *as;
201 	int fd = -1;
202 
203 	if (persist)
204 		fd = open("/dev/tty", O_RDWR);
205 	if (fd != -1) {
206 		if (ioctl(fd, TIOCCHKVERAUTH) == 0)
207 			goto good;
208 	}
209 
210 	if (!(as = auth_userchallenge(myname, login_style, "auth-doas",
211 	    &challenge)))
212 		errx(1, "Authorization failed");
213 	if (!challenge) {
214 		char host[HOST_NAME_MAX + 1];
215 		if (gethostname(host, sizeof(host)))
216 			snprintf(host, sizeof(host), "?");
217 		snprintf(cbuf, sizeof(cbuf),
218 		    "\rdoas (%.32s@%.32s) password: ", myname, host);
219 		challenge = cbuf;
220 	}
221 	response = readpassphrase(challenge, rbuf, sizeof(rbuf),
222 	    RPP_REQUIRE_TTY);
223 	if (response == NULL && errno == ENOTTY) {
224 		syslog(LOG_AUTHPRIV | LOG_NOTICE,
225 		    "tty required for %s", myname);
226 		errx(1, "a tty is required");
227 	}
228 	if (!auth_userresponse(as, response, 0)) {
229 		explicit_bzero(rbuf, sizeof(rbuf));
230 		syslog(LOG_AUTHPRIV | LOG_NOTICE,
231 		    "failed auth for %s", myname);
232 		errx(1, "Authorization failed");
233 	}
234 	explicit_bzero(rbuf, sizeof(rbuf));
235 good:
236 	if (fd != -1) {
237 		int secs = 5 * 60;
238 		ioctl(fd, TIOCSETVERAUTH, &secs);
239 		close(fd);
240 	}
241 }
242 
243 int
244 unveilcommands(const char *ipath, const char *cmd)
245 {
246 	char *path = NULL, *p;
247 	int unveils = 0;
248 
249 	if (strchr(cmd, '/') != NULL) {
250 		if (unveil(cmd, "x") != -1)
251 			unveils++;
252 		goto done;
253 	}
254 
255 	if (!ipath) {
256 		errno = ENOENT;
257 		goto done;
258 	}
259 	path = strdup(ipath);
260 	if (!path) {
261 		errno = ENOENT;
262 		goto done;
263 	}
264 	for (p = path; p && *p; ) {
265 		char buf[PATH_MAX];
266 		char *cp = strsep(&p, ":");
267 
268 		if (cp) {
269 			int r = snprintf(buf, sizeof buf, "%s/%s", cp, cmd);
270 			if (r != -1 && r < sizeof buf) {
271 				if (unveil(buf, "x") != -1)
272 					unveils++;
273 			}
274 		}
275 	}
276 done:
277 	free(path);
278 	return (unveils);
279 }
280 
281 int
282 main(int argc, char **argv)
283 {
284 	const char *safepath = "/bin:/sbin:/usr/bin:/usr/sbin:"
285 	    "/usr/local/bin:/usr/local/sbin";
286 	const char *confpath = NULL;
287 	char *shargv[] = { NULL, NULL };
288 	char *sh;
289 	const char *p;
290 	const char *cmd;
291 	char cmdline[LINE_MAX];
292 	char mypwbuf[_PW_BUF_LEN], targpwbuf[_PW_BUF_LEN];
293 	struct passwd mypwstore, targpwstore;
294 	struct passwd *mypw, *targpw;
295 	const struct rule *rule;
296 	uid_t uid;
297 	uid_t target = 0;
298 	gid_t groups[NGROUPS_MAX + 1];
299 	int ngroups;
300 	int i, ch, rv;
301 	int sflag = 0;
302 	int nflag = 0;
303 	char cwdpath[PATH_MAX];
304 	const char *cwd;
305 	char *login_style = NULL;
306 	char **envp;
307 
308 	setprogname("doas");
309 
310 	closefrom(STDERR_FILENO + 1);
311 
312 	uid = getuid();
313 
314 	while ((ch = getopt(argc, argv, "a:C:Lnsu:")) != -1) {
315 		switch (ch) {
316 		case 'a':
317 			login_style = optarg;
318 			break;
319 		case 'C':
320 			confpath = optarg;
321 			break;
322 		case 'L':
323 			i = open("/dev/tty", O_RDWR);
324 			if (i != -1)
325 				ioctl(i, TIOCCLRVERAUTH);
326 			exit(i == -1);
327 		case 'u':
328 			if (parseuid(optarg, &target) != 0)
329 				errx(1, "unknown user");
330 			break;
331 		case 'n':
332 			nflag = 1;
333 			break;
334 		case 's':
335 			sflag = 1;
336 			break;
337 		default:
338 			usage();
339 			break;
340 		}
341 	}
342 	argv += optind;
343 	argc -= optind;
344 
345 	if (confpath) {
346 		if (sflag)
347 			usage();
348 	} else if ((!sflag && !argc) || (sflag && argc))
349 		usage();
350 
351 	rv = getpwuid_r(uid, &mypwstore, mypwbuf, sizeof(mypwbuf), &mypw);
352 	if (rv != 0)
353 		err(1, "getpwuid_r failed");
354 	if (mypw == NULL)
355 		errx(1, "no passwd entry for self");
356 	ngroups = getgroups(NGROUPS_MAX, groups);
357 	if (ngroups == -1)
358 		err(1, "can't get groups");
359 	groups[ngroups++] = getgid();
360 
361 	if (sflag) {
362 		sh = getenv("SHELL");
363 		if (sh == NULL || *sh == '\0') {
364 			shargv[0] = mypw->pw_shell;
365 		} else
366 			shargv[0] = sh;
367 		argv = shargv;
368 		argc = 1;
369 	}
370 
371 	if (confpath) {
372 		checkconfig(confpath, argc, argv, uid, groups, ngroups,
373 		    target);
374 		exit(1);	/* fail safe */
375 	}
376 
377 	if (geteuid())
378 		errx(1, "not installed setuid");
379 
380 	parseconfig("/etc/doas.conf", 1);
381 
382 	/* cmdline is used only for logging, no need to abort on truncate */
383 	(void)strlcpy(cmdline, argv[0], sizeof(cmdline));
384 	for (i = 1; i < argc; i++) {
385 		if (strlcat(cmdline, " ", sizeof(cmdline)) >= sizeof(cmdline))
386 			break;
387 		if (strlcat(cmdline, argv[i], sizeof(cmdline)) >= sizeof(cmdline))
388 			break;
389 	}
390 
391 	cmd = argv[0];
392 	if (!permit(uid, groups, ngroups, &rule, target, cmd,
393 	    (const char **)argv + 1)) {
394 		syslog(LOG_AUTHPRIV | LOG_NOTICE,
395 		    "failed command for %s: %s", mypw->pw_name, cmdline);
396 		errc(1, EPERM, NULL);
397 	}
398 
399 	if (!(rule->options & NOPASS)) {
400 		if (nflag)
401 			errx(1, "Authorization required");
402 
403 		authuser(mypw->pw_name, login_style, rule->options & PERSIST);
404 	}
405 
406 	if ((p = getenv("PATH")) != NULL)
407 		formerpath = strdup(p);
408 	if (formerpath == NULL)
409 		formerpath = "";
410 
411 	if (unveil(_PATH_LOGIN_CONF, "r") == -1)
412 		err(1, "unveil");
413 	if (rule->cmd) {
414 		if (setenv("PATH", safepath, 1) == -1)
415 			err(1, "failed to set PATH '%s'", safepath);
416 	}
417 	if (unveilcommands(getenv("PATH"), cmd) == 0)
418 		goto fail;
419 
420 	if (pledge("stdio rpath getpw exec id", NULL) == -1)
421 		err(1, "pledge");
422 
423 	rv = getpwuid_r(target, &targpwstore, targpwbuf, sizeof(targpwbuf), &targpw);
424 	if (rv != 0)
425 		err(1, "getpwuid_r failed");
426 	if (targpw == NULL)
427 		errx(1, "no passwd entry for target");
428 
429 	if (setusercontext(NULL, targpw, target, LOGIN_SETGROUP |
430 	    LOGIN_SETPATH |
431 	    LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK |
432 	    LOGIN_SETUSER) != 0)
433 		errx(1, "failed to set user context for target");
434 
435 	if (pledge("stdio rpath exec", NULL) == -1)
436 		err(1, "pledge");
437 
438 	if (getcwd(cwdpath, sizeof(cwdpath)) == NULL)
439 		cwd = "(failed)";
440 	else
441 		cwd = cwdpath;
442 
443 	if (pledge("stdio exec", NULL) == -1)
444 		err(1, "pledge");
445 
446 	syslog(LOG_AUTHPRIV | LOG_INFO, "%s ran command %s as %s from %s",
447 	    mypw->pw_name, cmdline, targpw->pw_name, cwd);
448 
449 	envp = prepenv(rule, mypw, targpw);
450 
451 	if (rule->cmd) {
452 		/* do this again after setusercontext reset it */
453 		if (setenv("PATH", safepath, 1) == -1)
454 			err(1, "failed to set PATH '%s'", safepath);
455 	}
456 	execvpe(cmd, argv, envp);
457 fail:
458 	if (errno == ENOENT)
459 		errx(1, "%s: command not found", cmd);
460 	err(1, "%s", cmd);
461 }
462