commit 330fe27c7fd1e395ae626f5b6e64cf527f0ae951 from: Stefan Sperling date: Fri Sep 01 14:19:21 2023 UTC add nshdoas to fix the 'enable' command when stdin is not a TTY nshdoas is based on doas(1). It reads /etc/doas.conf and asks for the root password in case no matching rule exists. This avoids the need to fall back on su(1) and makes it possible to use the 'enable' command when sending nsh commands on standard input. ok Tom commit - 05ca4b38cd54ee0825808837fb61a618a9d5f894 commit + 330fe27c7fd1e395ae626f5b6e64cf527f0ae951 blob - 452d823c43405e462983e5c13eee67d7081cf956 blob + 46621f5ef859b66fa6d9d8d03658b3274837aa1a --- Makefile +++ Makefile @@ -1,7 +1,7 @@ # PROG= nsh -SUBDIR += bgpnsh +SUBDIR += bgpnsh nshdoas .PHONY: release dist @@ -11,7 +11,6 @@ SUBDIR += bgpnsh DEBUG?=-O0 -g .endif -NSH_REXEC_PATH?=/usr/local/bin/nsh .if make(install) DESTDIR?=/usr/local @@ -23,7 +22,7 @@ MANDIR?=/man/man #CFLAGS=-O -DDHCPLEASES=\"/flash/dhcpd.leases\" -Wmissing-prototypes -Wformat -Wall -Wpointer-arith -Wbad-function-cast #-W CFLAGS?=-O CFLAGS+=-Wmissing-prototypes -Wformat -Wall -Wbad-function-cast -I/usr/local/include #-W -Wpointer-arith -CPPFLAGS+=-DNSH_VERSION=${NSH_VERSION} -DNSH_REXEC_PATH=${NSH_REXEC_PATH} +CPPFLAGS+=-DNSH_VERSION=${NSH_VERSION} SRCS=arp.c compile.c main.c genget.c commands.c bgpcommands.c stats.c kroute.c SRCS+=ctl.c show.c if.c version.c route.c conf.c complete.c ieee80211.c blob - d4f2f7232f6db50ce16ca840c67378a609085eb5 blob + 1dc1723750e610a7038f9c2052303baf89c17234 --- bgpnsh/Makefile +++ bgpnsh/Makefile @@ -1,7 +1,7 @@ # PROG= bgpnsh -.PATH=${.CURDIR}/.. +.PATH=${.CURDIR}/..:${.CURDIR} .include "../nsh-version.mk" blob - e3a2faba7bbc29b9f4aabf953849d7af19c066a1 blob + f2b65a168c2ad92a6763a28bfe15066b933f20f1 --- cmdargs.c +++ cmdargs.c @@ -22,7 +22,9 @@ #include #include +#include #include +#include #include #include #include @@ -54,12 +56,18 @@ cmdargs(char *cmd, char *arg[]) int cmdargs_output(char *cmd, char *arg[], int stdoutfd, int stderrfd) { - return cmdargs_output_setenv(cmd, arg, stdoutfd, stderrfd, NULL); + return cmdargs_output_setenv(cmd, arg, stdoutfd, stderrfd, NULL, 0); } int +cmdargs_nowait(char *cmd, char *arg[], int pipefd) +{ + return cmdargs_output_setenv(cmd, arg, -1, -1, NULL, pipefd); +} + +int cmdargs_output_setenv(char *cmd, char *arg[], int stdoutfd, int stderrfd, - char **env) + char **env, int pipefd) { sig_t sigint, sigquit, sigchld; int status = -1; @@ -110,6 +118,8 @@ cmdargs_output_setenv(char *cmd, char *arg[], int stdo } break; default: + if (pipefd) + return 0; signal(SIGALRM, sigalarm); wait(&status); /* Wait for cmd to complete */ if (WIFEXITED(status)) /* normal exit? */ @@ -127,6 +137,31 @@ cmdargs_output_setenv(char *cmd, char *arg[], int stdo } int +cmdargs_wait_for_child(void) +{ + sig_t sigint, sigquit, sigchld; + int status = 127; + + sigint = signal(SIGINT, SIG_IGN); + sigquit = signal(SIGQUIT, SIG_IGN); + sigchld = signal(SIGCHLD, SIG_DFL); + + if (child != -1) { + signal(SIGALRM, sigalarm); + wait(&status); /* Wait for cmd to complete */ + if (WIFEXITED(status)) /* normal exit? */ + status = WEXITSTATUS(status); /* exit code */ + signal(SIGINT, sigint); + signal(SIGQUIT, sigquit); + signal(SIGCHLD, sigchld); + signal(SIGALRM, SIG_DFL); + child = -1; + } + + return status; +} + +int nsh_setrtable(int rtableid) { int ret = 0; blob - 58bb2bcedb2df9ec04f76bac63813f7681b224cf blob + 2e4977cfe088faa4a96086ab42f1262d2a78f9c1 --- externs.h +++ externs.h @@ -11,8 +11,17 @@ #define NSH_VERSION_STR NSH_STRINGVAL(NSH_VERSION) +#ifndef NSH_REXEC_PATH +#define NSH_REXEC_PATH /usr/local/bin/nsh +#endif + +#ifndef NSHDOAS_PATH +#define NSHDOAS_PATH /usr/local/libexec/nshdoas +#endif + #define NSH_REXEC_PATH_STR NSH_STRINGVAL(NSH_REXEC_PATH) #define NSH_REXEC_EXIT_CODE_QUIT 42 +#define NSHDOAS_PATH_STR NSH_STRINGVAL(NSHDOAS_PATH) #define NO_ARG(x) (strcasecmp(x, "no") == 0) /* absolute "no" */ @@ -152,9 +161,11 @@ extern char metricnames[]; /* ctl.c declarations moved to ctl.h */ /* cmdargs.c */ -int cmdargs_output_setenv(char *, char **, int, int, char **); +int cmdargs_output_setenv(char *, char **, int, int, char **, int); int cmdargs_output(char *, char **, int, int); int cmdargs(char *, char **); +int cmdargs_nowait(char *, char **, int); +int cmdargs_wait_for_child(void); int nsh_setrtable(int); /* commands.c */ blob - 6f63bce2a2d59e0fb1587fba9c14ace3e83be81c blob + 7bd989ba345e025a9879dd507c0390f836e45189 --- main.c +++ main.c @@ -205,6 +205,17 @@ main(int argc, char *argv[]) if (!privexec) printf("%% NSH v%s\n", vers); + } else { + /* + * We need line-buffered mode to cross seamlessly into + * the privileged process in case an 'enable' command + * appears on standard input. + */ + if (setvbuf(stdin, NULL, _IOLBF, 0) != 0) { + /* should not happen, warn just in case */ + printf("%% failed to enable line-buffered mode " + "for stdin\n"); + } } /* create temporal tables (if they aren't already there) */ blob - af71bd01fd67ae20381615194fddba516473b430 blob + 6eb375fbb75af8281fcd5619915fb64f2123f566 --- passwd.c +++ passwd.c @@ -32,6 +32,7 @@ #include #include #include +#include #include "externs.h" int read_pass(char *, size_t); @@ -277,6 +278,21 @@ enable_passwd(int argc, char **argv) printf("%% Too many arguments\n"); return 0; } + + return 0; +} + +static int +write_line(const char *line, size_t len, int fd) +{ + ssize_t written = 0, w; + + do { + w = write(fd, line, len); + if (w == -1) + return -1; + written += w; + } while (written < len); return 0; } @@ -285,13 +301,12 @@ int enable(int argc, char **argv) { char *doas_argv[] = { - DOAS, NSH_REXEC_PATH_STR, "-e", NULL - }; - char *su_argv[] = { - SU, "root", "-c", NSH_REXEC_PATH_STR " -e", NULL - + NSHDOAS_PATH_STR, NULL, NULL, NULL }; + char buf[64]; int exit_code; + int pfd[2]; + pid_t pid; if (argc != 1) return enable_passwd(argc, argv); @@ -304,36 +319,51 @@ enable(int argc, char **argv) return 0; } + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pfd) == -1) { + printf("%% socketpair failed: %s\n", strerror(errno)); + return 0; + } + + if (!interactive_mode) { + snprintf(buf, sizeof(buf), "%d", pfd[0]); + doas_argv[1] = "-F"; + doas_argv[2] = buf; + } + /* * Start an nsh child process in privileged mode. * The 'priv' flag will remain at zero in our own process. */ - printf("%% Obtaining root privileges via %s\n", DOAS); - exit_code = cmdargs(doas_argv[0], doas_argv); - if (exit_code == 0) + pid = cmdargs_nowait(doas_argv[0], doas_argv, pfd[0]); + if (pid == -1) return 0; - else if (exit_code == NSH_REXEC_EXIT_CODE_QUIT) { - /* The child exited due to a 'quit' command. */ - quit(); - } - /* - * XXX We cannot differentiate a doas exit code of 1 from an - * nsh exit code of 1. Under normal circumstances nsh will exit - * with code zero. Just assume that doas failed to run the - * command if we get here and retry with su. - */ + close(pfd[0]); - printf("%% Obtaining root privileges via %s\n", SU); - exit_code = cmdargs(su_argv[0], su_argv); + if (!interactive_mode) { + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; - if (exit_code == -1 || exit_code == 127) { - printf("%% Entering privileged mode failed: " - "Could not re-execute nsh\n"); - } else if (exit_code == NSH_REXEC_EXIT_CODE_QUIT) { + while ((linelen = getline(&line, &linesize, stdin)) != -1) { + if (write_line(line, linelen, pfd[1]) == -1) { + printf("%% writing to privileged child: %s\n", + strerror(errno)); + break; + } + if (strcmp(line, "disable\n") == 0) + break; + } + free(line); + } + + exit_code = cmdargs_wait_for_child(); + if (exit_code == 0) + return 0; + else if (exit_code == NSH_REXEC_EXIT_CODE_QUIT) { /* The child exited due to a 'quit' command. */ quit(); - } else if (exit_code) { + } else { printf("%% Privileged mode child process exited " "with error code %d\n", exit_code); } blob - /dev/null blob + 66fa671ac5ac8390f2891dc49b5bc3f1b2b477f3 (mode 644) --- /dev/null +++ nshdoas/.gitignore @@ -0,0 +1 @@ +obj/** blob - /dev/null blob + 89ad14b1c093f2143b239a8c9c2000cc7feeefad (mode 644) --- /dev/null +++ nshdoas/Makefile @@ -0,0 +1,32 @@ +# +PROG= nshdoas + +.PATH=${.CURDIR}/.. + +.include "../nsh-version.mk" + +.if ${NSH_RELEASE} != Yes +DEBUG?=-O0 -g +.endif + +.if make(install) +DESTDIR?=/usr/local +BINDIR?=/libexec +MANDIR?=/man/man +.endif + +CFLAGS+=-Wmissing-prototypes -Wformat -Wall -Wbad-function-cast +CPPFLAGS+=-DNSH_VERSION=${NSH_VERSION} +CPPFLAGS+=-I${.CURDIR}/.. -I ${.CURDIR} + +SRCS=parse.y nshdoas.c env.c +LDADD=-lutil + +BINOWN= root +BINMODE=4555 + +YFLAGS= + +NOMAN=yes + +.include blob - /dev/null blob + b98fe353b18676f6509ebe00978b5e3754e305b7 (mode 644) --- /dev/null +++ nshdoas/doas.h @@ -0,0 +1,49 @@ +/* $OpenBSD: doas.h,v 1.19 2021/11/30 20:08:15 tobias Exp $ */ +/* + * Copyright (c) 2015 Ted Unangst + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +struct rule { + int action; + int options; + const char *ident; + const char *target; + const char *cmd; + const char **cmdargs; + const char **envlist; +}; + +extern struct rule **rules; +extern size_t nrules; +extern int parse_error; + +extern const char *formerpath; + +struct passwd; + +char **prepenv(const struct rule *, const struct passwd *, + const struct passwd *); + +#define PERMIT 1 +#define DENY 2 + +#define NOPASS 0x1 +#define KEEPENV 0x2 +#define PERSIST 0x4 +#define NOLOG 0x8 + +#define AUTH_FAILED -1 +#define AUTH_OK 0 +#define AUTH_RETRIES 3 blob - /dev/null blob + 2d93a4089b6b4ea71ec6c922c88f22b79d15a6d6 (mode 644) --- /dev/null +++ nshdoas/env.c @@ -0,0 +1,235 @@ +/* $OpenBSD: env.c,v 1.10 2019/07/07 19:21:28 tedu Exp $ */ +/* + * Copyright (c) 2016 Ted Unangst + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "doas.h" + +const char *formerpath; + +struct envnode { + RB_ENTRY(envnode) node; + const char *key; + const char *value; +}; + +struct env { + RB_HEAD(envtree, envnode) root; + u_int count; +}; + +static void fillenv(struct env *env, const char **envlist); + +static int +envcmp(struct envnode *a, struct envnode *b) +{ + return strcmp(a->key, b->key); +} +RB_GENERATE_STATIC(envtree, envnode, node, envcmp) + +static struct envnode * +createnode(const char *key, const char *value) +{ + struct envnode *node; + + node = malloc(sizeof(*node)); + if (!node) + err(1, NULL); + node->key = strdup(key); + node->value = strdup(value); + if (!node->key || !node->value) + err(1, NULL); + return node; +} + +static void +freenode(struct envnode *node) +{ + free((char *)node->key); + free((char *)node->value); + free(node); +} + +static void +addnode(struct env *env, const char *key, const char *value) +{ + struct envnode *node; + + node = createnode(key, value); + RB_INSERT(envtree, &env->root, node); + env->count++; +} + +static struct env * +createenv(const struct rule *rule, const struct passwd *mypw, + const struct passwd *targpw) +{ + static const char *copyset[] = { + "DISPLAY", "TERM", + NULL + }; + struct env *env; + u_int i; + + env = malloc(sizeof(*env)); + if (!env) + err(1, NULL); + RB_INIT(&env->root); + env->count = 0; + + addnode(env, "DOAS_USER", mypw->pw_name); + addnode(env, "HOME", targpw->pw_dir); + addnode(env, "LOGNAME", targpw->pw_name); + addnode(env, "PATH", getenv("PATH")); + addnode(env, "SHELL", targpw->pw_shell); + addnode(env, "USER", targpw->pw_name); + + fillenv(env, copyset); + + if (rule->options & KEEPENV) { + extern const char **environ; + + for (i = 0; environ[i] != NULL; i++) { + struct envnode *node; + const char *e, *eq; + size_t len; + char name[1024]; + + e = environ[i]; + + /* ignore invalid or overlong names */ + if ((eq = strchr(e, '=')) == NULL || eq == e) + continue; + len = eq - e; + if (len > sizeof(name) - 1) + continue; + memcpy(name, e, len); + name[len] = '\0'; + + node = createnode(name, eq + 1); + if (RB_INSERT(envtree, &env->root, node)) { + /* ignore any later duplicates */ + freenode(node); + } else { + env->count++; + } + } + } + + return env; +} + +static char ** +flattenenv(struct env *env) +{ + char **envp; + struct envnode *node; + u_int i; + + envp = reallocarray(NULL, env->count + 1, sizeof(char *)); + if (!envp) + err(1, NULL); + i = 0; + RB_FOREACH(node, envtree, &env->root) { + if (asprintf(&envp[i], "%s=%s", node->key, node->value) == -1) + err(1, NULL); + i++; + } + envp[i] = NULL; + return envp; +} + +static void +fillenv(struct env *env, const char **envlist) +{ + struct envnode *node, key; + const char *e, *eq; + const char *val; + char name[1024]; + u_int i; + size_t len; + + for (i = 0; envlist[i]; i++) { + e = envlist[i]; + + /* parse out env name */ + if ((eq = strchr(e, '=')) == NULL) + len = strlen(e); + else + len = eq - e; + if (len > sizeof(name) - 1) + continue; + memcpy(name, e, len); + name[len] = '\0'; + + /* delete previous copies */ + key.key = name; + if (*name == '-') + key.key = name + 1; + if ((node = RB_FIND(envtree, &env->root, &key))) { + RB_REMOVE(envtree, &env->root, node); + freenode(node); + env->count--; + } + if (*name == '-') + continue; + + /* assign value or inherit from environ */ + if (eq) { + val = eq + 1; + if (*val == '$') { + if (strcmp(val + 1, "PATH") == 0) + val = formerpath; + else + val = getenv(val + 1); + } + } else { + if (strcmp(name, "PATH") == 0) + val = formerpath; + else + val = getenv(name); + } + /* at last, we have something to insert */ + if (val) { + node = createnode(name, val); + RB_INSERT(envtree, &env->root, node); + env->count++; + } + } +} + +char ** +prepenv(const struct rule *rule, const struct passwd *mypw, + const struct passwd *targpw) +{ + struct env *env; + + env = createenv(rule, mypw, targpw); + if (rule->envlist) + fillenv(env, rule->envlist); + + return flattenenv(env); +} blob - /dev/null blob + 17c1440bb3ba28fd3d976677c1d582586f2aebd6 (mode 644) --- /dev/null +++ nshdoas/nshdoas.c @@ -0,0 +1,405 @@ +/* $OpenBSD: doas.c,v 1.98 2022/12/22 19:53:22 kn Exp $ */ +/* + * Copyright (c) 2015 Ted Unangst + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "externs.h" + +#include "doas.h" + +static void __dead +usage(void) +{ + fprintf(stderr, "usage: nshdoas [-n] [-a style]\n"); + exit(1); +} + +static int +parseuid(const char *s, uid_t *uid) +{ + struct passwd *pw; + const char *errstr; + + if ((pw = getpwnam(s)) != NULL) { + *uid = pw->pw_uid; + if (*uid == UID_MAX) + return -1; + return 0; + } + *uid = strtonum(s, 0, UID_MAX - 1, &errstr); + if (errstr) + return -1; + return 0; +} + +static int +uidcheck(const char *s, uid_t desired) +{ + uid_t uid; + + if (parseuid(s, &uid) != 0) + return -1; + if (uid != desired) + return -1; + return 0; +} + +static int +parsegid(const char *s, gid_t *gid) +{ + struct group *gr; + const char *errstr; + + if ((gr = getgrnam(s)) != NULL) { + *gid = gr->gr_gid; + if (*gid == GID_MAX) + return -1; + return 0; + } + *gid = strtonum(s, 0, GID_MAX - 1, &errstr); + if (errstr) + return -1; + return 0; +} + +static int +match(uid_t uid, gid_t *groups, int ngroups, uid_t target, const char *cmd, + const char **cmdargs, struct rule *r) +{ + int i; + + if (r->ident[0] == ':') { + gid_t rgid; + if (parsegid(r->ident + 1, &rgid) == -1) + return 0; + for (i = 0; i < ngroups; i++) { + if (rgid == groups[i]) + break; + } + if (i == ngroups) + return 0; + } else { + if (uidcheck(r->ident, uid) != 0) + return 0; + } + if (r->target && uidcheck(r->target, target) != 0) + return 0; + if (r->cmd) { + if (strcmp(r->cmd, cmd)) + return 0; + if (r->cmdargs) { + /* if arguments were given, they should match explicitly */ + for (i = 0; r->cmdargs[i]; i++) { + if (!cmdargs[i]) + return 0; + if (strcmp(r->cmdargs[i], cmdargs[i])) + return 0; + } + if (cmdargs[i]) + return 0; + } + } + return 1; +} + +static int +permit(uid_t uid, gid_t *groups, int ngroups, const struct rule **lastr, + uid_t target, const char *cmd, const char **cmdargs) +{ + size_t i; + + *lastr = NULL; + for (i = 0; i < nrules; i++) { + if (match(uid, groups, ngroups, target, cmd, + cmdargs, rules[i])) + *lastr = rules[i]; + } + if (!*lastr) + return 0; + return (*lastr)->action; +} + +static void +parseconfig(const char *filename, int checkperms) +{ + extern FILE *yyfp; + extern int yyparse(void); + struct stat sb; + + yyfp = fopen(filename, "r"); + if (!yyfp) + err(1, checkperms ? "doas is not enabled, %s" : + "could not open config file %s", filename); + + if (checkperms) { + if (fstat(fileno(yyfp), &sb) != 0) + err(1, "fstat(\"%s\")", filename); + if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0) + errx(1, "%s is writable by group or other", filename); + if (sb.st_uid != 0) + errx(1, "%s is not owned by root", filename); + } + + yyparse(); + fclose(yyfp); + if (parse_error) + exit(1); +} + +static int +authuser_checkpass(char *myname, char *login_style) +{ + char *challenge = NULL, *response, rbuf[1024], cbuf[128]; + auth_session_t *as; + + if (!(as = auth_userchallenge(myname, login_style, "auth-doas", + &challenge))) { + warnx("Authentication failed"); + return AUTH_FAILED; + } + if (!challenge) { + char host[HOST_NAME_MAX + 1]; + + if (gethostname(host, sizeof(host))) + snprintf(host, sizeof(host), "?"); + snprintf(cbuf, sizeof(cbuf), + "%s \%.32s@%.32s password: ", + getprogname(), myname, host); + challenge = cbuf; + } + response = readpassphrase(challenge, rbuf, sizeof(rbuf), + RPP_REQUIRE_TTY); + if (response == NULL && errno == ENOTTY) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, + "tty required for %s", myname); + errx(1, "a tty is required"); + } + if (!auth_userresponse(as, response, 0)) { + explicit_bzero(rbuf, sizeof(rbuf)); + syslog(LOG_AUTHPRIV | LOG_NOTICE, + "failed auth for %s", myname); + warnx("Authentication failed"); + return AUTH_FAILED; + } + explicit_bzero(rbuf, sizeof(rbuf)); + return AUTH_OK; +} + +static void +authuser(char *myname, char *login_style, int persist) +{ + int i, fd = -1; + + if (persist) + fd = open("/dev/tty", O_RDWR); + if (fd != -1) { + if (ioctl(fd, TIOCCHKVERAUTH) == 0) + goto good; + } + for (i = 0; i < AUTH_RETRIES; i++) { + if (authuser_checkpass(myname, login_style) == AUTH_OK) + goto good; + } + exit(1); +good: + if (fd != -1) { + int secs = 5 * 60; + ioctl(fd, TIOCSETVERAUTH, &secs); + close(fd); + } +} + +int +main(int argc, char **argv) +{ + const char *p; + char *const cmd[] = { NSH_REXEC_PATH_STR, "-e", NULL }; + char mypwbuf[_PW_BUF_LEN], targpwbuf[_PW_BUF_LEN]; + struct passwd mypwstore, targpwstore; + struct passwd *mypw, *targpw; + const struct rule *rule; + uid_t uid; + uid_t target = 0; + gid_t groups[NGROUPS_MAX + 1]; + int ngroups; + int ch, rv; + int nflag = 0; + char cwdpath[PATH_MAX]; + const char *cwd; + const char *errstr; + char *login_style = NULL; + char **envp = NULL; + int nshfd = -1, action; + + setprogname("nshdoas"); + + uid = getuid(); + + while ((ch = getopt(argc, argv, "a:F:n:")) != -1) { + switch (ch) { + case 'a': + login_style = optarg; + break; + case 'F': + nshfd = strtonum(optarg, STDERR_FILENO + 1, INT_MAX, + &errstr); + if (errstr) + err(1, "nshfd is %s", errstr); + printf("%s: nshfd is %d\n", getprogname(), nshfd); + break; + case 'n': + nflag = 1; + break; + default: + usage(); + break; + } + } + argv += optind; + argc -= optind; + + if (argc) + usage(); + + if (nshfd == -1) + closefrom(STDERR_FILENO + 1); + else + closefrom(nshfd + 1); + + rv = getpwuid_r(uid, &mypwstore, mypwbuf, sizeof(mypwbuf), &mypw); + if (rv != 0) + err(1, "getpwuid_r failed"); + if (mypw == NULL) + errx(1, "no passwd entry for self"); + ngroups = getgroups(NGROUPS_MAX, groups); + if (ngroups == -1) + err(1, "can't get groups"); + groups[ngroups++] = getgid(); + + if (geteuid()) + errx(1, "not installed setuid"); + + rv = getpwuid_r(target, &targpwstore, targpwbuf, sizeof(targpwbuf), &targpw); + if (rv != 0) + err(1, "getpwuid_r failed"); + if (targpw == NULL) + errx(1, "no passwd entry for target"); + + parseconfig("/etc/doas.conf", 1); + + action = permit(uid, groups, ngroups, &rule, target, cmd[0], + (const char **)cmd + 1); + if (action == 0) { + printf("%% No rule for %s found in /etc/doas.conf; " + "root password required\n", mypw->pw_name); + authuser(targpw->pw_name, login_style, 0); + rule = NULL; + } else { + if (action != PERMIT) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, + "command not permitted for %s: %s", + mypw->pw_name, cmd[0]); + errc(1, EPERM, NULL); + } + if (!(rule->options & NOPASS)) { + if (nflag) + errx(1, "Authentication required"); + + authuser(mypw->pw_name, login_style, + rule->options & PERSIST); + } + } + + if ((p = getenv("PATH")) != NULL) + formerpath = strdup(p); + if (formerpath == NULL) + formerpath = ""; + + if (unveil(_PATH_LOGIN_CONF, "r") == -1) + err(1, "unveil %s", _PATH_LOGIN_CONF); + if (unveil(_PATH_LOGIN_CONF ".db", "r") == -1) + err(1, "unveil %s.db", _PATH_LOGIN_CONF); + if (unveil(_PATH_LOGIN_CONF_D, "r") == -1) + err(1, "unveil %s", _PATH_LOGIN_CONF_D); + if (unveil(NSH_REXEC_PATH_STR, "x") == -1) + err(1, "unveil %s", NSH_REXEC_PATH_STR); + + if (pledge("stdio rpath getpw exec id", NULL) == -1) + err(1, "pledge"); + + if (setusercontext(NULL, targpw, target, LOGIN_SETGROUP | + LOGIN_SETPATH | + LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK | + LOGIN_SETUSER | LOGIN_SETENV | LOGIN_SETRTABLE) != 0) + errx(1, "failed to set user context for target"); + + if (pledge("stdio rpath exec", NULL) == -1) + err(1, "pledge"); + + if (getcwd(cwdpath, sizeof(cwdpath)) == NULL) + cwd = "(failed)"; + else + cwd = cwdpath; + + if (pledge("stdio exec", NULL) == -1) + err(1, "pledge"); + + if (rule == NULL || !(rule->options & NOLOG)) { + syslog(LOG_AUTHPRIV | LOG_INFO, + "%s ran command %s as %s from %s", + mypw->pw_name, cmd[0], targpw->pw_name, cwd); + } + + if (rule) + envp = prepenv(rule, mypw, targpw); + + /* setusercontext set path for the next process, so reset it for us */ + if (setenv("PATH", formerpath, 1) == -1) + err(1, "failed to set PATH '%s'", formerpath); + + if (nshfd != -1) { + /* + * Redirect stdin to the nsh process pipe. + * Commands will arrive here. + */ + fpurge(stdin); + if (dup2(nshfd, STDIN_FILENO) == -1) + err(1, "dup2"); + } + + execvpe(cmd[0], cmd, envp); + if (errno == ENOENT) + errx(1, "%s: command not found", cmd[0]); + err(1, "%s", cmd[0]); +} blob - /dev/null blob + 604becb5445077766203a899c2923610e2745869 (mode 644) --- /dev/null +++ nshdoas/parse.y @@ -0,0 +1,354 @@ +/* $OpenBSD: parse.y,v 1.31 2022/03/22 20:36:49 deraadt Exp $ */ +/* + * Copyright (c) 2015 Ted Unangst + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +%{ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "doas.h" + +typedef struct { + union { + struct { + int action; + int options; + const char *cmd; + const char **cmdargs; + const char **envlist; + }; + const char **strlist; + const char *str; + }; + unsigned long lineno; + unsigned long colno; +} yystype; +#define YYSTYPE yystype + +FILE *yyfp; + +struct rule **rules; +size_t nrules; +static size_t maxrules; + +int parse_error = 0; + +static void yyerror(const char *, ...); +static int yylex(void); + +static size_t +arraylen(const char **arr) +{ + size_t cnt = 0; + + while (*arr) { + cnt++; + arr++; + } + return cnt; +} + +%} + +%token TPERMIT TDENY TAS TCMD TARGS +%token TNOPASS TNOLOG TPERSIST TKEEPENV TSETENV +%token TSTRING + +%% + +grammar: /* empty */ + | grammar '\n' + | grammar rule '\n' + | error '\n' + ; + +rule: action ident target cmd { + struct rule *r; + + r = calloc(1, sizeof(*r)); + if (!r) + errx(1, "can't allocate rule"); + r->action = $1.action; + r->options = $1.options; + r->envlist = $1.envlist; + r->ident = $2.str; + r->target = $3.str; + r->cmd = $4.cmd; + r->cmdargs = $4.cmdargs; + if (nrules == maxrules) { + if (maxrules == 0) + maxrules = 32; + rules = reallocarray(rules, maxrules, + 2 * sizeof(*rules)); + if (!rules) + errx(1, "can't allocate rules"); + maxrules *= 2; + } + rules[nrules++] = r; + } ; + +action: TPERMIT options { + $$.action = PERMIT; + $$.options = $2.options; + $$.envlist = $2.envlist; + } | TDENY { + $$.action = DENY; + $$.options = 0; + $$.envlist = NULL; + } ; + +options: /* none */ { + $$.options = 0; + $$.envlist = NULL; + } | options option { + $$.options = $1.options | $2.options; + $$.envlist = $1.envlist; + if (($$.options & (NOPASS|PERSIST)) == (NOPASS|PERSIST)) { + yyerror("can't combine nopass and persist"); + YYERROR; + } + if ($2.envlist) { + if ($$.envlist) { + yyerror("can't have two setenv sections"); + YYERROR; + } else + $$.envlist = $2.envlist; + } + } ; +option: TNOPASS { + $$.options = NOPASS; + $$.envlist = NULL; + } | TNOLOG { + $$.options = NOLOG; + $$.envlist = NULL; + } | TPERSIST { + $$.options = PERSIST; + $$.envlist = NULL; + } | TKEEPENV { + $$.options = KEEPENV; + $$.envlist = NULL; + } | TSETENV '{' strlist '}' { + $$.options = 0; + $$.envlist = $3.strlist; + } ; + +strlist: /* empty */ { + if (!($$.strlist = calloc(1, sizeof(char *)))) + errx(1, "can't allocate strlist"); + } | strlist TSTRING { + int nstr = arraylen($1.strlist); + + if (!($$.strlist = reallocarray($1.strlist, nstr + 2, + sizeof(char *)))) + errx(1, "can't allocate strlist"); + $$.strlist[nstr] = $2.str; + $$.strlist[nstr + 1] = NULL; + } ; + + +ident: TSTRING { + $$.str = $1.str; + } ; + +target: /* optional */ { + $$.str = NULL; + } | TAS TSTRING { + $$.str = $2.str; + } ; + +cmd: /* optional */ { + $$.cmd = NULL; + $$.cmdargs = NULL; + } | TCMD TSTRING args { + $$.cmd = $2.str; + $$.cmdargs = $3.cmdargs; + } ; + +args: /* empty */ { + $$.cmdargs = NULL; + } | TARGS strlist { + $$.cmdargs = $2.strlist; + } ; + +%% + +void +yyerror(const char *fmt, ...) +{ + va_list va; + + fprintf(stderr, "doas: "); + va_start(va, fmt); + vfprintf(stderr, fmt, va); + va_end(va); + fprintf(stderr, " at line %lu\n", yylval.lineno + 1); + parse_error = 1; +} + +static struct keyword { + const char *word; + int token; +} keywords[] = { + { "deny", TDENY }, + { "permit", TPERMIT }, + { "as", TAS }, + { "cmd", TCMD }, + { "args", TARGS }, + { "nopass", TNOPASS }, + { "nolog", TNOLOG }, + { "persist", TPERSIST }, + { "keepenv", TKEEPENV }, + { "setenv", TSETENV }, +}; + +int +yylex(void) +{ + char buf[1024], *ebuf, *p, *str; + int c, quoted = 0, quotes = 0, qerr = 0, escape = 0, nonkw = 0; + unsigned long qpos = 0; + size_t i; + + p = buf; + ebuf = buf + sizeof(buf); + +repeat: + /* skip whitespace first */ + for (c = getc(yyfp); c == ' ' || c == '\t'; c = getc(yyfp)) + yylval.colno++; + + /* check for special one-character constructions */ + switch (c) { + case '\n': + yylval.colno = 0; + yylval.lineno++; + /* FALLTHROUGH */ + case '{': + case '}': + return c; + case '#': + /* skip comments; NUL is allowed; no continuation */ + while ((c = getc(yyfp)) != '\n') + if (c == EOF) + goto eof; + yylval.colno = 0; + yylval.lineno++; + return c; + case EOF: + goto eof; + } + + /* parsing next word */ + for (;; c = getc(yyfp), yylval.colno++) { + switch (c) { + case '\0': + yyerror("unallowed character NUL in column %lu", + yylval.colno + 1); + escape = 0; + continue; + case '\\': + escape = !escape; + if (escape) + continue; + break; + case '\n': + if (quotes && !qerr) { + yyerror("unterminated quotes in column %lu", + qpos + 1); + qerr = 1; + } + if (escape) { + nonkw = 1; + escape = 0; + yylval.colno = ULONG_MAX; + yylval.lineno++; + continue; + } + goto eow; + case EOF: + if (escape) + yyerror("unterminated escape in column %lu", + yylval.colno); + if (quotes && !qerr) + yyerror("unterminated quotes in column %lu", + qpos + 1); + goto eow; + case '{': + case '}': + case '#': + case ' ': + case '\t': + if (!escape && !quotes) + goto eow; + break; + case '"': + if (!escape) { + quoted = 1; + quotes = !quotes; + if (quotes) { + nonkw = 1; + qerr = 0; + qpos = yylval.colno; + } + continue; + } + } + *p++ = c; + if (p == ebuf) { + yyerror("too long line"); + p = buf; + } + escape = 0; + } + +eow: + *p = 0; + if (c != EOF) + ungetc(c, yyfp); + if (p == buf) { + /* + * There could be a number of reasons for empty buffer, + * and we handle all of them here, to avoid cluttering + * the main loop. + */ + if (c == EOF) + goto eof; + else if (!quoted) /* accept, e.g., empty args: cmd foo args "" */ + goto repeat; + } + if (!nonkw) { + for (i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) { + if (strcmp(buf, keywords[i].word) == 0) + return keywords[i].token; + } + } + if ((str = strdup(buf)) == NULL) + err(1, "%s", __func__); + yylval.str = str; + return TSTRING; + +eof: + if (ferror(yyfp)) + yyerror("input error reading config"); + return 0; +}