128019Sjoerg/* $OpenBSD: doas.c,v 1.99 2024/02/15 18:57:58 tedu Exp $ */ 228021Sjoerg/* 328021Sjoerg * Copyright (c) 2015 Ted Unangst <tedu@openbsd.org> 428021Sjoerg * 528021Sjoerg * Permission to use, copy, modify, and distribute this software for any 628021Sjoerg * purpose with or without fee is hereby granted, provided that the above 728021Sjoerg * copyright notice and this permission notice appear in all copies. 828021Sjoerg * 928021Sjoerg * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 1028021Sjoerg * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 1128021Sjoerg * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 1228021Sjoerg * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 1328021Sjoerg * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 1428021Sjoerg * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 1528021Sjoerg * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 1628021Sjoerg */ 1728021Sjoerg 1828021Sjoerg#include <sys/types.h> 1928021Sjoerg#include <sys/stat.h> 2028021Sjoerg#include <sys/ioctl.h> 2128021Sjoerg 2228021Sjoerg#include <limits.h> 2328019Sjoerg#include <login_cap.h> 2428019Sjoerg#include <bsd_auth.h> 2528021Sjoerg#include <readpassphrase.h> 2628019Sjoerg#include <string.h> 2728019Sjoerg#include <stdio.h> 2828019Sjoerg#include <stdlib.h> 2928019Sjoerg#include <err.h> 3028019Sjoerg#include <unistd.h> 3128019Sjoerg#include <pwd.h> 3228019Sjoerg#include <grp.h> 3328019Sjoerg#include <syslog.h> 3428019Sjoerg#include <errno.h> 3528019Sjoerg#include <fcntl.h> 3628019Sjoerg 3728019Sjoerg#include "doas.h" 3828019Sjoerg 3928019Sjoergstatic void __dead 4028019Sjoergusage(void) 4128019Sjoerg{ 4228019Sjoerg fprintf(stderr, "usage: doas [-Lns] [-a style] [-C config] [-u user]" 4328019Sjoerg " command [arg ...]\n"); 4428019Sjoerg exit(1); 4528019Sjoerg} 4628019Sjoerg 4728019Sjoergstatic int 4828019Sjoergparseuid(const char *s, uid_t *uid) 4928019Sjoerg{ 5028019Sjoerg struct passwd *pw; 5128019Sjoerg const char *errstr; 5228019Sjoerg 5328019Sjoerg if ((pw = getpwnam(s)) != NULL) { 54111010Snectar *uid = pw->pw_uid; 5528019Sjoerg if (*uid == UID_MAX) 5628021Sjoerg return -1; 57111010Snectar return 0; 5828019Sjoerg } 59111010Snectar *uid = strtonum(s, 0, UID_MAX - 1, &errstr); 6028021Sjoerg if (errstr) 6128019Sjoerg return -1; 6292986Sobrien return 0; 6328019Sjoerg} 6471579Sdeischen 6528019Sjoergstatic int 6628019Sjoerguidcheck(const char *s, uid_t desired) 67122830Snectar{ 6879664Sdd uid_t uid; 6928019Sjoerg 7048614Sobrien if (parseuid(s, &uid) != 0) 7171579Sdeischen return -1; 7271579Sdeischen if (uid != desired) 7328021Sjoerg return -1; 7428019Sjoerg return 0; 75112156Smtm} 7648614Sobrien 7728021Sjoergstatic int 7828019Sjoergparsegid(const char *s, gid_t *gid) 7948614Sobrien{ 80112156Smtm struct group *gr; 8128019Sjoerg const char *errstr; 8228021Sjoerg 8328021Sjoerg if ((gr = getgrnam(s)) != NULL) { 8428021Sjoerg *gid = gr->gr_gid; 8528021Sjoerg if (*gid == GID_MAX) 8653941Sache return -1; 8772168Sphantom return 0; 8828019Sjoerg } 8928021Sjoerg *gid = strtonum(s, 0, GID_MAX - 1, &errstr); 9028021Sjoerg if (errstr) 9128021Sjoerg return -1; 9228021Sjoerg return 0; 9328019Sjoerg} 9428021Sjoerg 9528019Sjoergstatic int 9628021Sjoergmatch(uid_t uid, gid_t *groups, int ngroups, uid_t target, const char *cmd, 9728164Sache const char **cmdargs, struct rule *r) 9828164Sache{ 9928021Sjoerg int i; 10028021Sjoerg 10128021Sjoerg if (r->ident[0] == ':') { 10228021Sjoerg gid_t rgid; 10328021Sjoerg if (parsegid(r->ident + 1, &rgid) == -1) 10428019Sjoerg return 0; 10553941Sache for (i = 0; i < ngroups; i++) { 10653941Sache if (rgid == groups[i]) 10753941Sache break; 10828021Sjoerg } 10928021Sjoerg if (i == ngroups) 11028021Sjoerg return 0; 11128021Sjoerg } else { 11228021Sjoerg if (uidcheck(r->ident, uid) != 0) 11328021Sjoerg return 0; 11428021Sjoerg } 11528019Sjoerg if (r->target && uidcheck(r->target, target) != 0) 11653941Sache return 0; 117112156Smtm if (r->cmd) { 11828021Sjoerg if (strcmp(r->cmd, cmd)) 11928021Sjoerg return 0; 12028021Sjoerg if (r->cmdargs) { 12128019Sjoerg /* if arguments were given, they should match explicitly */ 12253941Sache for (i = 0; r->cmdargs[i]; i++) { 12353941Sache if (!cmdargs[i]) 12453941Sache return 0; 12553941Sache if (strcmp(r->cmdargs[i], cmdargs[i])) 12654316Ssheldonh return 0; 12754316Ssheldonh } 12854316Ssheldonh if (cmdargs[i]) 12953941Sache return 0; 13053941Sache } 13154316Ssheldonh } 13253941Sache return 1; 13353941Sache} 13453941Sache 13553941Sachestatic int 13653941Sachepermit(uid_t uid, gid_t *groups, int ngroups, const struct rule **lastr, 13753941Sache uid_t target, const char *cmd, const char **cmdargs) 13853941Sache{ 13928021Sjoerg size_t i; 140112156Smtm 14128021Sjoerg *lastr = NULL; 14228021Sjoerg for (i = 0; i < nrules; i++) { 14328021Sjoerg if (match(uid, groups, ngroups, target, cmd, 14428019Sjoerg cmdargs, rules[i])) 14528021Sjoerg *lastr = rules[i]; 146112156Smtm } 14728021Sjoerg if (!*lastr) 14828021Sjoerg return -1; 14928021Sjoerg if ((*lastr)->action == PERMIT) 15028019Sjoerg return 0; 15153941Sache return -1; 15253960Sache} 15353960Sache 15453941Sachestatic void 15553941Sacheparseconfig(const char *filename, int checkperms) 15653941Sache{ 15753941Sache extern FILE *yyfp; 15853960Sache extern int yyparse(void); 15953960Sache struct stat sb; 16053941Sache 16153941Sache yyfp = fopen(filename, "r"); 16253941Sache if (!yyfp) 16353960Sache err(1, checkperms ? "doas is not enabled, %s" : 164112156Smtm "could not open config file %s", filename); 16574412Sache 16674412Sache if (checkperms) { 16774412Sache if (fstat(fileno(yyfp), &sb) != 0) 16874412Sache err(1, "fstat(\"%s\")", filename); 16928021Sjoerg if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0) 170112156Smtm errx(1, "%s is writable by group or other", filename); 17128021Sjoerg if (sb.st_uid != 0) 17228021Sjoerg errx(1, "%s is not owned by root", filename); 17328021Sjoerg } 17428019Sjoerg 17528021Sjoerg yyparse(); 176112156Smtm fclose(yyfp); 17728021Sjoerg if (parse_error) 17828021Sjoerg exit(1); 17928021Sjoerg} 18028019Sjoerg 18128021Sjoergstatic void __dead 182112156Smtmcheckconfig(const char *confpath, int argc, char **argv, 18328021Sjoerg uid_t uid, gid_t *groups, int ngroups, uid_t target) 18428021Sjoerg{ 18528021Sjoerg const struct rule *rule; 18628019Sjoerg int rv; 18728021Sjoerg 188112156Smtm setresuid(uid, uid, uid); 18928021Sjoerg if (pledge("stdio rpath getpw", NULL) == -1) 19028021Sjoerg err(1, "pledge"); 19128021Sjoerg parseconfig(confpath, 0); 19228019Sjoerg if (!argc) 19328021Sjoerg exit(0); 194112156Smtm rv = permit(uid, groups, ngroups, &rule, target, argv[0], 19528021Sjoerg (const char **)argv + 1); 19628021Sjoerg if (rv == 0) { 19728021Sjoerg printf("permit%s\n", (rule->options & NOPASS) ? " nopass" : ""); 19828019Sjoerg exit(0); 19928021Sjoerg } else { 20028164Sache printf("deny\n"); 20128021Sjoerg exit(1); 20228019Sjoerg } 20354316Ssheldonh} 20454316Ssheldonh 20528021Sjoergstatic int 20628021Sjoergauthuser_checkpass(char *myname, char *login_style) 20754316Ssheldonh{ 20828021Sjoerg char *challenge = NULL, *response, rbuf[1024], cbuf[128]; 20953083Ssheldonh auth_session_t *as; 21028021Sjoerg 21128019Sjoerg if (!(as = auth_userchallenge(myname, login_style, "auth-doas", 21253083Ssheldonh &challenge))) { 21328021Sjoerg warnx("Authentication failed"); 21428019Sjoerg return AUTH_FAILED; 21528021Sjoerg } 21628021Sjoerg if (!challenge) { 21728164Sache char host[HOST_NAME_MAX + 1]; 21828021Sjoerg 21928019Sjoerg if (gethostname(host, sizeof(host))) 22028164Sache snprintf(host, sizeof(host), "?"); 22128021Sjoerg snprintf(cbuf, sizeof(cbuf), 22228019Sjoerg "\rdoas (%.32s@%.32s) password: ", myname, host); 22354316Ssheldonh challenge = cbuf; 22454316Ssheldonh } 22528021Sjoerg response = readpassphrase(challenge, rbuf, sizeof(rbuf), 22628021Sjoerg RPP_REQUIRE_TTY); 22754316Ssheldonh if (response == NULL && errno == ENOTTY) { 22828021Sjoerg syslog(LOG_AUTHPRIV | LOG_NOTICE, 22928019Sjoerg "tty required for %s", myname); 23053083Ssheldonh errx(1, "a tty is required"); 23153083Ssheldonh } 23253083Ssheldonh if (!auth_userresponse(as, response, 0)) { 23328021Sjoerg explicit_bzero(rbuf, sizeof(rbuf)); 23453083Ssheldonh syslog(LOG_AUTHPRIV | LOG_NOTICE, 23553083Ssheldonh "failed auth for %s", myname); 23653083Ssheldonh warnx("Authentication failed"); 23728021Sjoerg return AUTH_FAILED; 23853083Ssheldonh } 23928019Sjoerg explicit_bzero(rbuf, sizeof(rbuf)); 24028164Sache return AUTH_OK; 24128164Sache} 24228021Sjoerg 24328021Sjoergstatic void 24428019Sjoergauthuser(char *myname, char *login_style, int persist) 24528021Sjoerg{ 24628021Sjoerg int i, fd = -1; 24728021Sjoerg 24828021Sjoerg if (persist) 24954316Ssheldonh fd = open("/dev/tty", O_RDWR); 25054316Ssheldonh if (fd != -1) { 25154316Ssheldonh if (ioctl(fd, TIOCCHKVERAUTH) == 0) 25254316Ssheldonh goto good; 25354316Ssheldonh } 25454316Ssheldonh for (i = 0; i < AUTH_RETRIES; i++) { 25554316Ssheldonh if (authuser_checkpass(myname, login_style) == AUTH_OK) 25654316Ssheldonh goto good; 25728164Sache } 25828021Sjoerg exit(1); 25928019Sjoerggood: 26054316Ssheldonh if (fd != -1) { 26154316Ssheldonh int secs = 5 * 60; 26228021Sjoerg ioctl(fd, TIOCSETVERAUTH, &secs); 26328021Sjoerg close(fd); 26454316Ssheldonh } 26528021Sjoerg} 26628021Sjoerg 26728021Sjoergint 26828021Sjoergunveilcommands(const char *ipath, const char *cmd) 26954301Ssheldonh{ 27028021Sjoerg char *path = NULL, *p; 27128019Sjoerg int unveils = 0; 27228021Sjoerg 27328019Sjoerg if (strchr(cmd, '/') != NULL) { 27428164Sache if (unveil(cmd, "x") != -1) 27528164Sache unveils++; 27628021Sjoerg goto done; 27728021Sjoerg } 27828019Sjoerg 27928021Sjoerg if (!ipath) { 28054316Ssheldonh errno = ENOENT; 28154316Ssheldonh goto done; 28254316Ssheldonh } 28354316Ssheldonh path = strdup(ipath); 28472168Sphantom if (!path) { 28572168Sphantom errno = ENOENT; 28628021Sjoerg goto done; 28728021Sjoerg } 28828021Sjoerg for (p = path; p && *p; ) { 28928021Sjoerg char buf[PATH_MAX]; 29028021Sjoerg char *cp = strsep(&p, ":"); 29128021Sjoerg 29228021Sjoerg if (cp) { 29328019Sjoerg int r = snprintf(buf, sizeof buf, "%s/%s", cp, cmd); 29472168Sphantom if (r >= 0 && r < sizeof buf) { 29572168Sphantom if (unveil(buf, "x") != -1) 29628021Sjoerg unveils++; 29728021Sjoerg } 29828021Sjoerg } 29928021Sjoerg } 30028021Sjoergdone: 30128021Sjoerg free(path); 30228021Sjoerg return (unveils); 30328019Sjoerg} 30428021Sjoerg 30528019Sjoergint 30628021Sjoergmain(int argc, char **argv) 30728021Sjoerg{ 30872168Sphantom const char *safepath = "/bin:/sbin:/usr/bin:/usr/sbin:" 30974409Sache "/usr/local/bin:/usr/local/sbin"; 31074409Sache const char *confpath = NULL; 31174409Sache char *shargv[] = { NULL, NULL }; 31274409Sache char *sh; 31374409Sache const char *p; 31474409Sache const char *cmd; 31574409Sache char cmdline[LINE_MAX]; 31674409Sache char mypwbuf[_PW_BUF_LEN], targpwbuf[_PW_BUF_LEN]; 31728021Sjoerg struct passwd mypwstore, targpwstore; 31872168Sphantom struct passwd *mypw, *targpw; 31928021Sjoerg const struct rule *rule; 32028019Sjoerg uid_t uid; 32128021Sjoerg uid_t target = 0; 32228021Sjoerg gid_t groups[NGROUPS_MAX + 1]; 32328021Sjoerg int ngroups; 32428019Sjoerg int i, ch, rv; 32553083Ssheldonh int sflag = 0; 32653083Ssheldonh int nflag = 0; 32753083Ssheldonh char cwdpath[PATH_MAX]; 32853083Ssheldonh const char *cwd; 32953083Ssheldonh char *login_style = NULL; 33053083Ssheldonh char **envp; 33153083Ssheldonh 33253083Ssheldonh setprogname("doas"); 33353083Ssheldonh 33453083Ssheldonh closefrom(STDERR_FILENO + 1); 33553083Ssheldonh 33654316Ssheldonh uid = getuid(); 33754316Ssheldonh 33853083Ssheldonh while ((ch = getopt(argc, argv, "a:C:Lnsu:")) != -1) { 33953083Ssheldonh switch (ch) { 34054316Ssheldonh case 'a': 34153083Ssheldonh login_style = optarg; 34253083Ssheldonh break; 34353083Ssheldonh case 'C': 34453083Ssheldonh confpath = optarg; 34553083Ssheldonh break; 34653083Ssheldonh case 'L': 34753083Ssheldonh i = open("/dev/tty", O_RDWR); 34853083Ssheldonh if (i != -1) 34953083Ssheldonh ioctl(i, TIOCCLRVERAUTH); 35053083Ssheldonh exit(i == -1); 35153083Ssheldonh case 'u': 35253083Ssheldonh if (parseuid(optarg, &target) != 0) 35353083Ssheldonh errx(1, "unknown user"); 35454316Ssheldonh break; 35553083Ssheldonh case 'n': 35653083Ssheldonh nflag = 1; 35753083Ssheldonh break; 35853083Ssheldonh case 's': 35953083Ssheldonh sflag = 1; 36053083Ssheldonh break; 36153083Ssheldonh default: 36253083Ssheldonh usage(); 36353083Ssheldonh break; 36453083Ssheldonh } 36528021Sjoerg } 36628021Sjoerg argv += optind; 36754316Ssheldonh argc -= optind; 36854316Ssheldonh 36954316Ssheldonh if (confpath) { 37054316Ssheldonh if (sflag) 37154316Ssheldonh usage(); 37254316Ssheldonh } else if ((!sflag && !argc) || (sflag && argc)) 37354316Ssheldonh usage(); 37454316Ssheldonh 37528164Sache rv = getpwuid_r(uid, &mypwstore, mypwbuf, sizeof(mypwbuf), &mypw); 37628021Sjoerg if (rv != 0) 37728019Sjoerg err(1, "getpwuid_r failed"); 37854316Ssheldonh if (mypw == NULL) 37954316Ssheldonh errx(1, "no passwd entry for self"); 38028021Sjoerg ngroups = getgroups(NGROUPS_MAX, groups); 38128021Sjoerg if (ngroups == -1) 38254316Ssheldonh err(1, "can't get groups"); 38328021Sjoerg groups[ngroups++] = getgid(); 38428021Sjoerg 38528021Sjoerg if (sflag) { 38628019Sjoerg sh = getenv("SHELL"); 38728021Sjoerg if (sh == NULL || *sh == '\0') { 38828019Sjoerg shargv[0] = mypw->pw_shell; 38928164Sache } else 39028164Sache shargv[0] = sh; 39128021Sjoerg argv = shargv; 39228021Sjoerg argc = 1; 39328019Sjoerg } 39428021Sjoerg 39528021Sjoerg if (confpath) { 39628021Sjoerg if (pledge("stdio rpath getpw id", NULL) == -1) 39772168Sphantom err(1, "pledge"); 39853941Sache checkconfig(confpath, argc, argv, uid, groups, ngroups, 39953941Sache target); 40072168Sphantom exit(1); /* fail safe */ 40153941Sache } 40272168Sphantom 40353941Sache if (geteuid()) 40453941Sache errx(1, "not installed setuid"); 40553941Sache 40653941Sache parseconfig("/etc/doas.conf", 1); 40774409Sache 40874409Sache /* cmdline is used only for logging, no need to abort on truncate */ 40974409Sache (void)strlcpy(cmdline, argv[0], sizeof(cmdline)); 41074409Sache for (i = 1; i < argc; i++) { 41174409Sache if (strlcat(cmdline, " ", sizeof(cmdline)) >= sizeof(cmdline)) 41274409Sache break; 41374409Sache if (strlcat(cmdline, argv[i], sizeof(cmdline)) >= sizeof(cmdline)) 41474409Sache break; 41553941Sache } 41628021Sjoerg 41772168Sphantom cmd = argv[0]; 41828021Sjoerg rv = permit(uid, groups, ngroups, &rule, target, cmd, 41928019Sjoerg (const char **)argv + 1); 42028021Sjoerg if (rv != 0) { 42128021Sjoerg syslog(LOG_AUTHPRIV | LOG_NOTICE, 42228021Sjoerg "command not permitted for %s: %s", mypw->pw_name, cmdline); 42328019Sjoerg errc(1, EPERM, NULL); 42428021Sjoerg } 42528164Sache 42628021Sjoerg if (!(rule->options & NOPASS)) { 42728019Sjoerg if (nflag) 42854316Ssheldonh errx(1, "Authentication required"); 42954316Ssheldonh 43028021Sjoerg authuser(mypw->pw_name, login_style, rule->options & PERSIST); 43128021Sjoerg } 43254316Ssheldonh 43328021Sjoerg if ((p = getenv("PATH")) != NULL) 43428021Sjoerg formerpath = strdup(p); 43528021Sjoerg if (formerpath == NULL) 43628019Sjoerg formerpath = ""; 43728021Sjoerg 43828019Sjoerg if (unveil(_PATH_LOGIN_CONF, "r") == -1) 43928164Sache err(1, "unveil %s", _PATH_LOGIN_CONF); 44028164Sache if (unveil(_PATH_LOGIN_CONF ".db", "r") == -1) 44128021Sjoerg err(1, "unveil %s.db", _PATH_LOGIN_CONF); 44228021Sjoerg if (unveil(_PATH_LOGIN_CONF_D, "r") == -1) 44328019Sjoerg err(1, "unveil %s", _PATH_LOGIN_CONF_D); 44479664Sdd if (rule->cmd) { 44579664Sdd if (setenv("PATH", safepath, 1) == -1) 44679664Sdd err(1, "failed to set PATH '%s'", safepath); 447122830Snectar } 448122830Snectar if (unveilcommands(getenv("PATH"), cmd) == 0) 44979664Sdd goto fail; 45079664Sdd 451122830Snectar if (pledge("stdio rpath getpw exec id", NULL) == -1) 452122830Snectar err(1, "pledge"); 453122830Snectar 454122830Snectar rv = getpwuid_r(target, &targpwstore, targpwbuf, sizeof(targpwbuf), &targpw); 455122830Snectar if (rv != 0) 45679664Sdd err(1, "getpwuid_r failed"); 457122830Snectar if (targpw == NULL) 458122830Snectar errx(1, "no passwd entry for target"); 45979664Sdd 46079664Sdd if (setusercontext(NULL, targpw, target, LOGIN_SETGROUP | 461112156Smtm LOGIN_SETPATH | 46279664Sdd LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK | 46379664Sdd LOGIN_SETUSER | LOGIN_SETENV | LOGIN_SETRTABLE) != 0) 46479664Sdd errx(1, "failed to set user context for target"); 46528021Sjoerg 46628021Sjoerg if (pledge("stdio rpath exec", NULL) == -1) 46728164Sache err(1, "pledge"); 46828021Sjoerg 46928019Sjoerg if (getcwd(cwdpath, sizeof(cwdpath)) == NULL) 47028164Sache cwd = "(failed)"; 47128021Sjoerg else 47228019Sjoerg cwd = cwdpath; 47354316Ssheldonh 47454316Ssheldonh if (pledge("stdio exec", NULL) == -1) 47528021Sjoerg err(1, "pledge"); 47628021Sjoerg 47754316Ssheldonh if (!(rule->options & NOLOG)) { 47828021Sjoerg syslog(LOG_AUTHPRIV | LOG_INFO, 47928021Sjoerg "%s ran command %s as %s from %s", 48028021Sjoerg mypw->pw_name, cmdline, targpw->pw_name, cwd); 48146051Swes } 48246042Swes 48328021Sjoerg envp = prepenv(rule, mypw, targpw); 48428021Sjoerg 48528019Sjoerg /* setusercontext set path for the next process, so reset it for us */ 48628021Sjoerg if (rule->cmd) { 48728019Sjoerg if (setenv("PATH", safepath, 1) == -1) 48828164Sache err(1, "failed to set PATH '%s'", safepath); 48928164Sache } else { 49028021Sjoerg if (setenv("PATH", formerpath, 1) == -1) 49128021Sjoerg err(1, "failed to set PATH '%s'", formerpath); 49248550Sobrien } 49348550Sobrien execvpe(cmd, argv, envp); 49448550Sobrienfail: 49548550Sobrien if (errno == ENOENT) 49648550Sobrien errx(1, "%s: command not found", cmd); 49748550Sobrien err(1, "%s", cmd); 49852860Sache} 49948550Sobrien