From 63916476e2cfa9402bbe21be29dee2a7d60d775e Mon Sep 17 00:00:00 2001 From: Ted Unangst Date: Thu, 16 Jul 2015 20:44:21 +0000 Subject: [PATCH] import doas. still subject to changes, large and small. --- Makefile | 14 +++ doas.1 | 57 ++++++++++ doas.c | 321 ++++++++++++++++++++++++++++++++++++++++++++++++++++ doas.conf.5 | 70 ++++++++++++ doas.h | 20 ++++ parse.y | 210 ++++++++++++++++++++++++++++++++++ 6 files changed, 692 insertions(+) create mode 100644 Makefile create mode 100644 doas.1 create mode 100644 doas.c create mode 100644 doas.conf.5 create mode 100644 doas.h create mode 100644 parse.y diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..191d00f --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +# $OpenBSD: Makefile,v 1.9 2014/01/13 01:41:00 tedu Exp $ + +SRCS= parse.y doas.c + +PROG= doas +MAN= doas.1 doas.conf.5 + +BINOWN= root +BINMODE=4555 + +CFLAGS+= -I${.CURDIR} +COPTS+= -Wall + +.include diff --git a/doas.1 b/doas.1 new file mode 100644 index 0000000..d2f94ef --- /dev/null +++ b/doas.1 @@ -0,0 +1,57 @@ +.\" $OpenBSD$ +.\" +.\"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. +.Dd $Mdocdate$ +.Dt DOAS 1 +.Os +.Sh NAME +.Nm doas +.Nd execute commands as another user +.Sh SYNOPSIS +.Nm doas +.Op Fl u Ar user +command +.Op Ar args +.Sh DESCRIPTION +The +.Nm +utility executes the given command as another user. +.Pp +The options are as follows: +.Bl -tag -width tenletters +.It Fl u Ar user +Execute the command as +.Ar user . +The default is root. +.El +.Sh EXIT STATUS +.Ex -std doas +It may fail because of one of the following reasons: +.Pp +.Bl -bullet -compact +.It +The config file could not be parsed. +.It +The user attempted an command which is not permitted. +.It +Entered passphrase is incorrect. +.El +.Sh HISTORY +The +.Nm +command first appeared in +.Ox 5.8 . +.Sh AUTHORS +.An Ted Unangst Aq Mt tedu@openbsd.org diff --git a/doas.c b/doas.c new file mode 100644 index 0000000..7c800bd --- /dev/null +++ b/doas.c @@ -0,0 +1,321 @@ +/* $OpenBSD$ */ +/* + * 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 "doas.h" + +static void __dead +usage(void) +{ + fprintf(stderr, "usage: doas [-u user] command [args]\n"); + exit(1); +} + +size_t +arraylen(const char **arr) +{ + size_t cnt = 0; + while (*arr) { + cnt++; + arr++; + } + return cnt; +} + +static int +parseuid(const char *s, uid_t *uid) +{ + struct passwd *pw; + const char *errstr; + + if ((pw = getpwnam(s)) != NULL) { + *uid = pw->pw_uid; + return 0; + } + *uid = strtonum(s, 0, UID_MAX, &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 gid_t +strtogid(const char *s) +{ + struct group *gr; + const char *errstr; + gid_t gid; + + if ((gr = getgrnam(s)) != NULL) + return gr->gr_gid; + gid = strtonum(s, 0, GID_MAX, &errstr); + if (errstr) + return -1; + return gid; +} + +static int +match(uid_t uid, gid_t *groups, int ngroups, uid_t target, const char *cmd, + struct rule *r) +{ + int i; + + if (r->ident[0] == ':') { + gid_t rgid = strtogid(r->ident + 1); + if (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 && strcmp(r->cmd, cmd) != 0) + return 0; + return 1; +} + +static int +permit(uid_t uid, gid_t *groups, int ngroups, struct rule **lastr, + uid_t target, const char *cmd) +{ + int i; + + *lastr = NULL; + for (i = 0; i < nrules; i++) { + if (match(uid, groups, ngroups, target, cmd, rules[i])) + *lastr = rules[i]; + } + if (!*lastr) + return 0; + return (*lastr)->action == PERMIT; +} + +static void +parseconfig(const char *filename) +{ + extern FILE *yyfp; + extern int yyparse(void); + + yyfp = fopen(filename, "r"); + if (!yyfp) { + fprintf(stderr, "doas is not enabled.\n"); + exit(1); + } + yyparse(); + fclose(yyfp); +} + +static int +copyenvhelper(const char **oldenvp, const char **safeset, int nsafe, char **envp, int ei) +{ + int i; + for (i = 0; i < nsafe; i++) { + const char **oe = oldenvp; + while (*oe) { + size_t len = strlen(safeset[i]); + if (strncmp(*oe, safeset[i], len) == 0 && + (*oe)[len] == '=') { + if (!(envp[ei++] = strdup(*oe))) + err(1, "strdup"); + break; + } + oe++; + } + } + return ei; +} + +static char ** +copyenv(const char **oldenvp, struct rule *rule) +{ + const char *safeset[] = { + "DISPLAY", "HOME", "LOGNAME", "MAIL", "SHELL", + "PATH", "TERM", "USER", "USERNAME", + NULL, + }; + int nsafe; + int nextras = 0; + char **envp; + const char **extra; + int ei; + int i, j; + + if ((rule->options & KEEPENV) && !rule->envlist) { + j = arraylen(oldenvp); + envp = reallocarray(NULL, j + 1, sizeof(char *)); + for (i = 0; i < j; i++) { + if (!(envp[i] = strdup(oldenvp[i]))) + err(1, "strdup"); + } + envp[i] = NULL; + return envp; + } + + nsafe = arraylen(safeset); + if ((extra = rule->envlist)) { + nextras = arraylen(extra); + for (i = 0; i < nsafe; i++) { + for (j = 0; j < nextras; j++) { + if (strcmp(extra[j], safeset[i]) == 0) { + extra[j--] = extra[nextras--]; + extra[nextras] = NULL; + } + } + } + } + + envp = reallocarray(NULL, nsafe + nextras + 1, sizeof(char *)); + if (!envp) + err(1, "can't allocate new environment"); + + ei = 0; + ei = copyenvhelper(oldenvp, safeset, nsafe, envp, ei); + ei = copyenvhelper(oldenvp, rule->envlist, nextras, envp, ei); + envp[ei] = NULL; + + return envp; +} + +static void __dead +fail(void) +{ + const char *msgs[] = { + "No lollygagging!", + "Better luck next time.", + "PEBKAC detected.", + "That's what happens when you're lazy.", + "It is clear that this has not been thought through.", + "That's the most ridiculous thing I've heard in the last two or three minutes!", + "No sane people allowed here. Go home.", + "I would explain, but I am too drunk.", + "You're not allowed to have an opinion.", + "Complaint forms are handled in another department.", + }; + const char *m; + + m = msgs[arc4random_uniform(sizeof(msgs) / sizeof(msgs[0]))]; + fprintf(stderr, m); + fprintf(stderr, "\n"); + exit(1); +} + +int +main(int argc, char **argv, char **envp) +{ + char cmdline[1024]; + char myname[32]; + uid_t uid, target = 0; + gid_t groups[NGROUPS_MAX + 1]; + int ngroups; + struct passwd *pw; + struct rule *rule; + const char *cmd; + int i, ch; + const char *safepath = "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"; + + parseconfig("/etc/doas.conf"); + + while ((ch = getopt(argc, argv, "u:")) != -1) { + switch (ch) { + case 'u': + if (parseuid(optarg, &target) != 0) + errx(1, "unknown user"); + break; + default: + usage(); + break; + } + } + argv += optind; + argc -= optind; + + if (!argc) + usage(); + + cmd = argv[0]; + strlcpy(cmdline, argv[0], sizeof(cmdline)); + for (i = 1; i < argc; i++) { + strlcat(cmdline, " ", sizeof(cmdline)); + strlcat(cmdline, argv[i], sizeof(cmdline)); + } + + uid = getuid(); + pw = getpwuid(uid); + if (!pw) + err(1, "getpwuid failed"); + strlcpy(myname, pw->pw_name, sizeof(myname)); + ngroups = getgroups(NGROUPS_MAX, groups); + if (ngroups == -1) + err(1, "can't get groups"); + groups[ngroups++] = getgid(); + + if (!permit(uid, groups, ngroups, &rule, target, cmd)) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "failed command for %s: %s", myname, cmdline); + fail(); + } + + if (!(rule->options & NOPASS)) { + if (!auth_userokay(myname, NULL, NULL, NULL)) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "failed password for %s", myname); + fail(); + } + } + envp = copyenv((const char **)envp, rule); + + pw = getpwuid(target); + if (!pw) + errx(1, "no passwd entry for target"); + if (setusercontext(NULL, pw, target, LOGIN_SETGROUP | + LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK | + LOGIN_SETUSER) != 0) + errx(1, "failed to set user context for target"); + + syslog(LOG_AUTHPRIV | LOG_INFO, "%s ran command as %s: %s", myname, pw->pw_name, cmdline); + setenv("PATH", safepath, 1); + execvpe(cmd, argv, envp); + err(1, "%s", cmd); +} diff --git a/doas.conf.5 b/doas.conf.5 new file mode 100644 index 0000000..3f25bd1 --- /dev/null +++ b/doas.conf.5 @@ -0,0 +1,70 @@ +.\" $OpenBSD$ +.\" +.\"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. +.Dd $Mdocdate$ +.Dt DOAS.CONF 5 +.Os +.Sh NAME +.Nm doas.conf +.Nd doas configuration file +.Sh DESCRIPTION +The +.Xr doas 1 +utility executes commands as other users according to the rules +in the +.Nm +configuration file. +.Pp +The rules have the following format: +.Bd -literal -offset indent +permit|deny [options] [identity] [as target] [cmd command] +.Ed +.Pp +Rules consist of the following parts: +.Bl -tag -width tenletters +.It permit|deny +The action to be taken if this rule matches. +.It options +Options are: +.Bl -tag -width tenletters +.It nopass +The user is not required to enter a password. +.It keepenv +The user's environment is maintained. +The default is to reset the environment. +.It keepenv { [variable names] } +Reset the environment, but keep the specified variables. +.El +.It identity +The username to match. +Groups may be specified by prepending a colon (:). +Numeric IDs are also accepted. +.It as target +The target user the running user is allowed to run the command as. +The default is root. +.It cmd command +The command the user is allowed or denied to run. +The default is all commands. +Be advised that it's best to specify absolute paths. +.El +.Pp +The last matching rule determines the action taken. +.Sh EXAMPLES +The following example permits users in group wheel to exeucte commands as root, +and additionally permits tedu to run procmap as root without a password. +.Bd -literal -offset indent +permit :wheel +permit nopass tedu cmd /usr/sbin/procmap +.Ed diff --git a/doas.h b/doas.h new file mode 100644 index 0000000..b6d0275 --- /dev/null +++ b/doas.h @@ -0,0 +1,20 @@ + +struct rule { + int action; + int options; + const char *ident; + const char *target; + const char *cmd; + const char **envlist; +}; + +extern struct rule **rules; +extern int nrules, maxrules; + +size_t arraylen(const char **); + +#define PERMIT 1 +#define DENY 2 + +#define NOPASS 0x1 +#define KEEPENV 0x2 diff --git a/parse.y b/parse.y new file mode 100644 index 0000000..4729b4e --- /dev/null +++ b/parse.y @@ -0,0 +1,210 @@ +/* $OpenBSD$ */ +/* + * 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 "doas.h" + +typedef struct { + union { + struct { + int action; + int options; + const char **envlist; + }; + const char *str; + }; +} yystype; +#define YYSTYPE yystype + +FILE *yyfp; + +struct rule **rules; +int nrules, maxrules; + +%} + +%token TPERMIT TDENY TAS TCMD +%token TNOPASS TKEEPENV +%token TSTRING + +%% + +grammar: /* empty */ + | grammar '\n' + | grammar rule '\n' + ; + +rule: action ident target cmd { + struct rule *r; + r = calloc(1, sizeof(*r)); + r->action = $1.action; + r->options = $1.options; + r->envlist = $1.envlist; + r->ident = $2.str; + r->target = $3.str; + r->cmd = $4.str; + if (nrules == maxrules) { + if (maxrules == 0) + maxrules = 63; + else + maxrules *= 2; + if (!(rules = reallocarray(rules, maxrules, sizeof(*rules)))) + errx(1, "can't allocate rules"); + } + rules[nrules++] = r; + } ; + +action: TPERMIT options { + $$.action = PERMIT; + $$.options = $2.options; + $$.envlist = $2.envlist; + } | TDENY { + $$.action = DENY; + } ; + +options: /* none */ + | options option { + $$.options = $1.options | $2.options; + $$.envlist = $1.envlist; + if ($2.envlist) { + if ($$.envlist) + errx(1, "can't have two keepenv sections"); + else + $$.envlist = $2.envlist; + } + } ; +option: TNOPASS { + $$.options = NOPASS; + } | TKEEPENV { + $$.options = KEEPENV; + } | TKEEPENV '{' envlist '}' { + $$.options = KEEPENV; + $$.envlist = $3.envlist; + } ; + +envlist: /* empty */ { + if (!($$.envlist = calloc(1, sizeof(char *)))) + errx(1, "can't allocate envlist"); + } | envlist TSTRING { + int nenv = arraylen($1.envlist); + if (!($$.envlist = reallocarray($1.envlist, nenv + 2, sizeof(char *)))) + errx(1, "can't allocate envlist"); + $$.envlist[nenv] = $2.str; + $$.envlist[nenv + 1] = NULL; + } + + +ident: TSTRING { + $$.str = $1.str; + } ; + +target: /* optional */ { + $$.str = NULL; + } | TAS TSTRING { + $$.str = $2.str; + } ; + +cmd: /* optional */ { + $$.str = NULL; + } | TCMD TSTRING { + $$.str = $2.str; + } ; + +%% + +void +yyerror(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + fprintf(stderr, "doas: "); + vfprintf(stderr, fmt, va); + fprintf(stderr, "\n"); + va_end(va); + exit(1); +} + +struct keyword { + const char *word; + int token; +} keywords[] = { + { "deny", TDENY }, + { "permit", TPERMIT }, + { "as", TAS }, + { "cmd", TCMD }, + { "nopass", TNOPASS }, + { "keepenv", TKEEPENV }, +}; + +int +yylex(void) +{ + char buf[1024], *ebuf, *p, *str; + int i, c; + + p = buf; + ebuf = buf + sizeof(buf); + while ((c = getc(yyfp)) == ' ' || c == '\t') + ; /* skip spaces */ + switch (c) { + case '\n': + case '{': + case '}': + return c; + case '#': + while ((c = getc(yyfp)) != '\n' && c != EOF) + ; /* skip comments */ + if (c == EOF) + return 0; + return c; + case EOF: + return 0; + case ':': + *p++ = c; + c = getc(yyfp); + break; + default: + break; + } + while (isalnum(c)) { + *p++ = c; + if (p == ebuf) + yyerror("too much stuff"); + c = getc(yyfp); + } + *p = 0; + if (c != EOF) + ungetc(c, yyfp); + 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, "strdup"); + yylval.str = str; + return TSTRING; +} -- 2.39.2