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