1 /* $OpenBSD: doas.c,v 1.99 2024/02/15 18:57:58 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
usage(void)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
parseuid(const char * s,uid_t * uid)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
uidcheck(const char * s,uid_t desired)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
parsegid(const char * s,gid_t * gid)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
match(uid_t uid,gid_t * groups,int ngroups,uid_t target,const char * cmd,const char ** cmdargs,struct rule * r)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
permit(uid_t uid,gid_t * groups,int ngroups,const struct rule ** lastr,uid_t target,const char * cmd,const char ** cmdargs)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 -1;
149 if ((*lastr)->action == PERMIT)
150 return 0;
151 return -1;
152 }
153
154 static void
parseconfig(const char * filename,int checkperms)155 parseconfig(const char *filename, int checkperms)
156 {
157 extern FILE *yyfp;
158 extern int yyparse(void);
159 struct stat sb;
160
161 yyfp = fopen(filename, "r");
162 if (!yyfp)
163 err(1, checkperms ? "doas is not enabled, %s" :
164 "could not open config file %s", filename);
165
166 if (checkperms) {
167 if (fstat(fileno(yyfp), &sb) != 0)
168 err(1, "fstat(\"%s\")", filename);
169 if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0)
170 errx(1, "%s is writable by group or other", filename);
171 if (sb.st_uid != 0)
172 errx(1, "%s is not owned by root", filename);
173 }
174
175 yyparse();
176 fclose(yyfp);
177 if (parse_error)
178 exit(1);
179 }
180
181 static void __dead
checkconfig(const char * confpath,int argc,char ** argv,uid_t uid,gid_t * groups,int ngroups,uid_t target)182 checkconfig(const char *confpath, int argc, char **argv,
183 uid_t uid, gid_t *groups, int ngroups, uid_t target)
184 {
185 const struct rule *rule;
186 int rv;
187
188 setresuid(uid, uid, uid);
189 if (pledge("stdio rpath getpw", NULL) == -1)
190 err(1, "pledge");
191 parseconfig(confpath, 0);
192 if (!argc)
193 exit(0);
194 rv = permit(uid, groups, ngroups, &rule, target, argv[0],
195 (const char **)argv + 1);
196 if (rv == 0) {
197 printf("permit%s\n", (rule->options & NOPASS) ? " nopass" : "");
198 exit(0);
199 } else {
200 printf("deny\n");
201 exit(1);
202 }
203 }
204
205 static int
authuser_checkpass(char * myname,char * login_style)206 authuser_checkpass(char *myname, char *login_style)
207 {
208 char *challenge = NULL, *response, rbuf[1024], cbuf[128];
209 auth_session_t *as;
210
211 if (!(as = auth_userchallenge(myname, login_style, "auth-doas",
212 &challenge))) {
213 warnx("Authentication failed");
214 return AUTH_FAILED;
215 }
216 if (!challenge) {
217 char host[HOST_NAME_MAX + 1];
218
219 if (gethostname(host, sizeof(host)))
220 snprintf(host, sizeof(host), "?");
221 snprintf(cbuf, sizeof(cbuf),
222 "\rdoas (%.32s@%.32s) password: ", myname, host);
223 challenge = cbuf;
224 }
225 response = readpassphrase(challenge, rbuf, sizeof(rbuf),
226 RPP_REQUIRE_TTY);
227 if (response == NULL && errno == ENOTTY) {
228 syslog(LOG_AUTHPRIV | LOG_NOTICE,
229 "tty required for %s", myname);
230 errx(1, "a tty is required");
231 }
232 if (!auth_userresponse(as, response, 0)) {
233 explicit_bzero(rbuf, sizeof(rbuf));
234 syslog(LOG_AUTHPRIV | LOG_NOTICE,
235 "failed auth for %s", myname);
236 warnx("Authentication failed");
237 return AUTH_FAILED;
238 }
239 explicit_bzero(rbuf, sizeof(rbuf));
240 return AUTH_OK;
241 }
242
243 static void
authuser(char * myname,char * login_style,int persist)244 authuser(char *myname, char *login_style, int persist)
245 {
246 int i, fd = -1;
247
248 if (persist)
249 fd = open("/dev/tty", O_RDWR);
250 if (fd != -1) {
251 if (ioctl(fd, TIOCCHKVERAUTH) == 0)
252 goto good;
253 }
254 for (i = 0; i < AUTH_RETRIES; i++) {
255 if (authuser_checkpass(myname, login_style) == AUTH_OK)
256 goto good;
257 }
258 exit(1);
259 good:
260 if (fd != -1) {
261 int secs = 5 * 60;
262 ioctl(fd, TIOCSETVERAUTH, &secs);
263 close(fd);
264 }
265 }
266
267 int
unveilcommands(const char * ipath,const char * cmd)268 unveilcommands(const char *ipath, const char *cmd)
269 {
270 char *path = NULL, *p;
271 int unveils = 0;
272
273 if (strchr(cmd, '/') != NULL) {
274 if (unveil(cmd, "x") != -1)
275 unveils++;
276 goto done;
277 }
278
279 if (!ipath) {
280 errno = ENOENT;
281 goto done;
282 }
283 path = strdup(ipath);
284 if (!path) {
285 errno = ENOENT;
286 goto done;
287 }
288 for (p = path; p && *p; ) {
289 char buf[PATH_MAX];
290 char *cp = strsep(&p, ":");
291
292 if (cp) {
293 int r = snprintf(buf, sizeof buf, "%s/%s", cp, cmd);
294 if (r >= 0 && r < sizeof buf) {
295 if (unveil(buf, "x") != -1)
296 unveils++;
297 }
298 }
299 }
300 done:
301 free(path);
302 return (unveils);
303 }
304
305 int
main(int argc,char ** argv)306 main(int argc, char **argv)
307 {
308 const char *safepath = "/bin:/sbin:/usr/bin:/usr/sbin:"
309 "/usr/local/bin:/usr/local/sbin";
310 const char *confpath = NULL;
311 char *shargv[] = { NULL, NULL };
312 char *sh;
313 const char *p;
314 const char *cmd;
315 char cmdline[LINE_MAX];
316 char mypwbuf[_PW_BUF_LEN], targpwbuf[_PW_BUF_LEN];
317 struct passwd mypwstore, targpwstore;
318 struct passwd *mypw, *targpw;
319 const struct rule *rule;
320 uid_t uid;
321 uid_t target = 0;
322 gid_t groups[NGROUPS_MAX + 1];
323 int ngroups;
324 int i, ch, rv;
325 int sflag = 0;
326 int nflag = 0;
327 char cwdpath[PATH_MAX];
328 const char *cwd;
329 char *login_style = NULL;
330 char **envp;
331
332 setprogname("doas");
333
334 closefrom(STDERR_FILENO + 1);
335
336 uid = getuid();
337
338 while ((ch = getopt(argc, argv, "a:C:Lnsu:")) != -1) {
339 switch (ch) {
340 case 'a':
341 login_style = optarg;
342 break;
343 case 'C':
344 confpath = optarg;
345 break;
346 case 'L':
347 i = open("/dev/tty", O_RDWR);
348 if (i != -1)
349 ioctl(i, TIOCCLRVERAUTH);
350 exit(i == -1);
351 case 'u':
352 if (parseuid(optarg, &target) != 0)
353 errx(1, "unknown user");
354 break;
355 case 'n':
356 nflag = 1;
357 break;
358 case 's':
359 sflag = 1;
360 break;
361 default:
362 usage();
363 break;
364 }
365 }
366 argv += optind;
367 argc -= optind;
368
369 if (confpath) {
370 if (sflag)
371 usage();
372 } else if ((!sflag && !argc) || (sflag && argc))
373 usage();
374
375 rv = getpwuid_r(uid, &mypwstore, mypwbuf, sizeof(mypwbuf), &mypw);
376 if (rv != 0)
377 err(1, "getpwuid_r failed");
378 if (mypw == NULL)
379 errx(1, "no passwd entry for self");
380 ngroups = getgroups(NGROUPS_MAX, groups);
381 if (ngroups == -1)
382 err(1, "can't get groups");
383 groups[ngroups++] = getgid();
384
385 if (sflag) {
386 sh = getenv("SHELL");
387 if (sh == NULL || *sh == '\0') {
388 shargv[0] = mypw->pw_shell;
389 } else
390 shargv[0] = sh;
391 argv = shargv;
392 argc = 1;
393 }
394
395 if (confpath) {
396 if (pledge("stdio rpath getpw id", NULL) == -1)
397 err(1, "pledge");
398 checkconfig(confpath, argc, argv, uid, groups, ngroups,
399 target);
400 exit(1); /* fail safe */
401 }
402
403 if (geteuid())
404 errx(1, "not installed setuid");
405
406 parseconfig("/etc/doas.conf", 1);
407
408 /* cmdline is used only for logging, no need to abort on truncate */
409 (void)strlcpy(cmdline, argv[0], sizeof(cmdline));
410 for (i = 1; i < argc; i++) {
411 if (strlcat(cmdline, " ", sizeof(cmdline)) >= sizeof(cmdline))
412 break;
413 if (strlcat(cmdline, argv[i], sizeof(cmdline)) >= sizeof(cmdline))
414 break;
415 }
416
417 cmd = argv[0];
418 rv = permit(uid, groups, ngroups, &rule, target, cmd,
419 (const char **)argv + 1);
420 if (rv != 0) {
421 syslog(LOG_AUTHPRIV | LOG_NOTICE,
422 "command not permitted for %s: %s", mypw->pw_name, cmdline);
423 errc(1, EPERM, NULL);
424 }
425
426 if (!(rule->options & NOPASS)) {
427 if (nflag)
428 errx(1, "Authentication required");
429
430 authuser(mypw->pw_name, login_style, rule->options & PERSIST);
431 }
432
433 if ((p = getenv("PATH")) != NULL)
434 formerpath = strdup(p);
435 if (formerpath == NULL)
436 formerpath = "";
437
438 if (unveil(_PATH_LOGIN_CONF, "r") == -1)
439 err(1, "unveil %s", _PATH_LOGIN_CONF);
440 if (unveil(_PATH_LOGIN_CONF ".db", "r") == -1)
441 err(1, "unveil %s.db", _PATH_LOGIN_CONF);
442 if (unveil(_PATH_LOGIN_CONF_D, "r") == -1)
443 err(1, "unveil %s", _PATH_LOGIN_CONF_D);
444 if (rule->cmd) {
445 if (setenv("PATH", safepath, 1) == -1)
446 err(1, "failed to set PATH '%s'", safepath);
447 }
448 if (unveilcommands(getenv("PATH"), cmd) == 0)
449 goto fail;
450
451 if (pledge("stdio rpath getpw exec id", NULL) == -1)
452 err(1, "pledge");
453
454 rv = getpwuid_r(target, &targpwstore, targpwbuf, sizeof(targpwbuf), &targpw);
455 if (rv != 0)
456 err(1, "getpwuid_r failed");
457 if (targpw == NULL)
458 errx(1, "no passwd entry for target");
459
460 if (setusercontext(NULL, targpw, target, LOGIN_SETGROUP |
461 LOGIN_SETPATH |
462 LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK |
463 LOGIN_SETUSER | LOGIN_SETENV | LOGIN_SETRTABLE) != 0)
464 errx(1, "failed to set user context for target");
465
466 if (pledge("stdio rpath exec", NULL) == -1)
467 err(1, "pledge");
468
469 if (getcwd(cwdpath, sizeof(cwdpath)) == NULL)
470 cwd = "(failed)";
471 else
472 cwd = cwdpath;
473
474 if (pledge("stdio exec", NULL) == -1)
475 err(1, "pledge");
476
477 if (!(rule->options & NOLOG)) {
478 syslog(LOG_AUTHPRIV | LOG_INFO,
479 "%s ran command %s as %s from %s",
480 mypw->pw_name, cmdline, targpw->pw_name, cwd);
481 }
482
483 envp = prepenv(rule, mypw, targpw);
484
485 /* setusercontext set path for the next process, so reset it for us */
486 if (rule->cmd) {
487 if (setenv("PATH", safepath, 1) == -1)
488 err(1, "failed to set PATH '%s'", safepath);
489 } else {
490 if (setenv("PATH", formerpath, 1) == -1)
491 err(1, "failed to set PATH '%s'", formerpath);
492 }
493 execvpe(cmd, argv, envp);
494 fail:
495 if (errno == ENOENT)
496 errx(1, "%s: command not found", cmd);
497 err(1, "%s", cmd);
498 }
499