X-Git-Url: https://git.distorted.org.uk/~mdw/become/blobdiff_plain/88e486d58fd7f8086bf6b7a9b3cf0127671edb33..b0f66028a9a17a845590a4fe737a4f5e46f6b778:/src/become.c diff --git a/src/become.c b/src/become.c index a690072..1edb89c 100644 --- a/src/become.c +++ b/src/become.c @@ -1,10 +1,10 @@ /* -*-c-*- * - * $Id: become.c,v 1.3 1997/08/07 16:28:59 mdw Exp $ + * $Id: become.c,v 1.25 2003/11/29 23:39:16 mdw Exp $ * * Main code for `become' * - * (c) 1997 EBI + * (c) 1998 EBI */ /*----- Licensing notice --------------------------------------------------* @@ -29,6 +29,94 @@ /*----- Revision history --------------------------------------------------* * * $Log: become.c,v $ + * Revision 1.25 2003/11/29 23:39:16 mdw + * Debianization. + * + * Revision 1.24 2003/10/15 09:27:06 mdw + * Make sure standard file descriptors are open before starting properly. + * + * Revision 1.23 2003/10/12 10:00:06 mdw + * Fix for daemon mode. Oops. + * + * Revision 1.22 2003/10/12 00:14:55 mdw + * Major overhaul. Now uses DSA signatures rather than the bogus symmetric + * encrypt-and-hope thing. Integrated with mLib and Catacomb. + * + * Revision 1.21 1999/07/28 09:31:01 mdw + * Empty path components are equivalent to `.'. + * + * Revision 1.20 1999/05/04 16:17:11 mdw + * Change to header file name for parser. See log for `parse.h' for + * details. + * + * Revision 1.19 1998/06/29 13:10:41 mdw + * Add some commentary regarding an issue in `sudo' which affects `become'; + * I'm not fixing it yet because I don't think it's important. + * + * Fixed the PATH lookup code to use the right binary name: `binary' rather + * than `todo[0]'. The two only differ when `style' is `l_login', in which + * case `binary' has an initial `/' anyway, and the PATH lookup code is + * never invoked. The name is used in a buffer-overflow precheck, though, + * and auditing is easier if the naming is consistent. + * + * Revision 1.18 1998/06/26 10:32:54 mdw + * Cosmetic change: use sizeof(destination) in memcpy. + * + * Revision 1.17 1998/06/18 15:06:59 mdw + * Close log before execing program to avoid leaving a socket open. + * + * Revision 1.16 1998/04/23 13:21:04 mdw + * Small tweaks. Support no-network configuration option, and rearrange + * the help text a little. + * + * Revision 1.15 1998/01/13 11:10:44 mdw + * Add `TZ' to the list of variables to be preserved. + * + * Revision 1.14 1998/01/12 16:45:39 mdw + * Fix copyright date. + * + * Revision 1.13 1997/09/26 09:14:57 mdw + * Merged blowfish branch into trunk. + * + * Revision 1.12 1997/09/25 16:04:48 mdw + * Change directory after becoming someone else, instead of before. This + * avoids problems with root-squashed NFS mounts. + * + * Revision 1.11.2.1 1997/09/26 09:07:58 mdw + * Use the Blowfish encryption algorithm instead of IDEA. This is partly + * because I prefer Blowfish (without any particularly strong evidence) but + * mainly because IDEA is patented and Blowfish isn't. + * + * Revision 1.11 1997/09/24 09:48:45 mdw + * Fix (scary) overrun bug in group allocation stuff. + * + * Revision 1.10 1997/09/17 10:14:10 mdw + * Fix a typo. Support service names in `--port' option. + * + * Revision 1.9 1997/09/10 10:28:05 mdw + * Allow default port to be given as a service name or port number. Handle + * groups properly (lots of options here). + * + * Revision 1.8 1997/09/08 13:56:24 mdw + * Change criteria for expunging items from the user's PATH: instead of + * removing things starting with `.', remove things not starting with `/'. + * + * Revision 1.7 1997/09/08 13:43:20 mdw + * Change userid when creating tracefiles rather than fiddling with + * `access': it works rather better. Also, insert some stdio buffer + * flushing to ensure tracedumps are completely written. + * + * Revision 1.6 1997/09/05 13:47:44 mdw + * Make the `-L' (trace-level) option's argument optional, like the long + * version is. + * + * Revision 1.5 1997/09/05 11:45:19 mdw + * Add support for different login styles, and environment variable + * manipulation in a safe and useful way. + * + * Revision 1.4 1997/08/20 16:15:13 mdw + * Overhaul of environment handling. Fix daft bug in path search code. + * * Revision 1.3 1997/08/07 16:28:59 mdw * Do something useful when users attempt to become themselves. * @@ -46,6 +134,7 @@ #include #include +#include #include #include #include @@ -63,12 +152,23 @@ #include #include +#include #include #include #include +#include extern char **environ; +/* --- mLib --- */ + +#include +#include +#include +#include +#include +#include + /* --- Local headers --- */ #include "become.h" @@ -76,13 +176,53 @@ extern char **environ; #include "check.h" #include "daemon.h" #include "lexer.h" -#include "mdwopt.h" #include "name.h" -#include "parser.h" +#include "parse.h" #include "rule.h" -#include "utils.h" #include "userdb.h" +/*----- Type definitions --------------------------------------------------*/ + +/* --- Symbol table entry for an environment variable --- */ + +typedef struct sym_env { + sym_base _base; /* Symbol table information */ + unsigned f; /* Flags word (see below) */ + char *val; /* Pointer to variable value */ +} sym_env; + +/* --- Environment variable flags --- */ + +enum { + envFlag_preserve = 1 +}; + +/* --- Login behaviour types --- */ + +#define l_preserve 0 /* Preserve the environment */ +#define l_setuser 1 /* Update who I am */ +#define l_login 2 /* Do a full login */ + +/* --- Group behaviour types --- * + * + * Note that these make a handy bitfield. + */ + +#ifdef HAVE_SETGROUPS + +enum { + g_unset = 0, /* Nobody's set a preference */ + g_keep = 1, /* Leave the group memberships */ + g_replace = 2, /* Replace group memberships */ + g_merge = (g_keep | g_replace) /* Merge the group memberships */ +}; + +#endif + +/*----- Static variables --------------------------------------------------*/ + +static sym_table bc__env; + /*----- Main code ---------------------------------------------------------*/ /* --- @bc__write@ --- * @@ -105,7 +245,7 @@ static void bc__write(FILE *fp, const char *p) /* --- Try to be a little efficient --- * * - * Gather up non-`$' characters using @strcpn@ and spew them out really + * Gather up non-`$' characters using @strcspn@ and spew them out really * quickly. */ @@ -121,6 +261,131 @@ static void bc__write(FILE *fp, const char *p) } } +/* --- @bc__setenv@ --- * + * + * Arguments: @sym_env *e@ = pointer to environment variable block + * @const char *val@ = value to set + * + * Returns: --- + * + * Use: Sets an environment variable block to the right value. + */ + +static void bc__setenv(sym_env *e, const char *val) +{ + e->val = xmalloc(strlen(e->_base.name) + 1 + strlen(val) + 1); + sprintf(e->val, "%s=%s", e->_base.name, val); +} + +/* --- @bc__putenv@ --- * + * + * Arguments: @const char *var@ = name of the variable to set, or 0 if + * this is embedded in the value string + * @const char *val@ = value to set, or 0 if the variable must + * be removed + * @unsigned int fl@ = flags to set + * @unsigned int force@ = force overwrite of preserved variables + * + * Returns: Pointer to symbol block, or zero if it was deleted. + * + * Use: Puts an item into the environment. + */ + +static sym_env *bc__putenv(const char *var, const char *val, + unsigned int fl, unsigned int force) +{ + unsigned int f; + sym_env *e; + char *q = 0; + size_t sz; + + /* --- Sort out embedded variable names --- */ + + if (!var) { + const char *p = strchr(val, '='); + + if (p) { + sz = p - val; + q = xmalloc(sz + 1); + memcpy(q, val, sz); + q[sz] = 0; + var = q; + val = p + 1; + } else { + var = val; + val = 0; + } + } + + /* --- Find the variable block --- */ + + if (val) { + e = sym_find(&bc__env, var, -1, sizeof(*e), &f); + if (!f || ~e->f & envFlag_preserve || force) { + if (f) + free(e->val); + bc__setenv(e, val); + } + } else { + e = sym_find(&bc__env, var, -1, 0, 0); + if (e && (force || ~e->f & envFlag_preserve)) + sym_remove(&bc__env, e); + e = 0; + } + + /* --- Tidy up and return --- */ + + if (q) + free(q); + if (e) + e->f = fl; + return (e); +} + +/* --- @bc__addGroups@ --- * + * + * Arguments: @gid_t *g@ = pointer to a group array + * @int *png@ = pointer to number of entries in the array + * @const gid_t *a@ = pointer to groups to add + * @int na@ = number of groups to add + * + * Returns: Zero if it was OK, nonzero if we should stop now. + * + * Use: Adds groups to a groups array. + */ + +static int bc__addGroups(gid_t *g, int *png, const gid_t *a, int na) +{ + int i, j; + int ng = *png; + + for (i = 0; i < na; i++) { + + /* --- Ensure this group isn't already in the list --- */ + + for (j = 0; j < ng; j++) { + if (a[i] == g[j]) + goto next_group; + } + + /* --- See if there's room for more --- */ + + if (ng >= NGROUPS_MAX) { + moan("too many groups (system limit exceeded) -- some have been lost"); + *png = ng; + return (-1); + } + + /* --- Add the group --- */ + + g[ng++] = a[i]; + next_group:; + } + + *png = ng; + return (0); +} + /* --- @bc__banner@ --- * * * Arguments: @FILE *fp@ = stream to write on @@ -149,38 +414,24 @@ static void bc__usage(FILE *fp) bc__write(fp, "Usage: \n" " $ -c \n" - " $ [ []...]\n" - " $ -d [-p ] [-f ]\n"); -} - -/* --- @bc__makeEnv@ --- * - * - * Arguments: @const char *var@ = name of an environment variable - * @const char *value@ = value of the variable - * - * Returns: A pointer to an allocated block assigning the value to the - * name. - * - * Use: Constructs environment mappings. - */ - -static char *bc__makeEnv(const char *var, const char *value) -{ - char *p = xmalloc(strlen(var) + strlen(value) + 2); - sprintf(p, "%s=%s", var, value); - return (p); + " $ [] [ []...]\n" +#ifndef NONETWORK + " $ -d [-p ] [-f ]\n" +#endif + ); } /* --- @bc__help@ --- * * * Arguments: @FILE *fp@ = stream to write on + * @int suid@ = whether we're running set-uid * * Returns: --- * * Use: Displays a help message for this excellent piece of software. */ -static void bc__help(FILE *fp) +static void bc__help(FILE *fp, int suid) { bc__banner(fp); putc('\n', fp); @@ -200,19 +451,50 @@ static void bc__help(FILE *fp) "\n" "Options available are:\n" "\n" -"-h, --help Display this help text\n" -"-v, --version Display the version number of this copy of $\n" -"-l, --login Really log in as the user\n" -"-c, --command=CMD Run the (Bourne) shell command CMD\n" -"-d, --daemon Start up a daemon, to accept requests from clients\n" -"-p, --port=PORT In daemon mode, listen on PORT\n" -"-f, --config-file=FILE In daemon mode, read config from FILE\n" -#ifdef TRACING -"--impersonate=USER Claim to be USER when asking the server\n" -"--trace=FILE Dump trace information to FILE (boring)\n" -"--trace-level=OPTS Set level of tracing information\n" +"-h, --help Display this help text\n" +"-u, --usage Display a short usage summary\n" +"-v, --version Display $'s version number\n" +"\n" +"-e, --preserve-environment Try to preserve the current environment\n" +"-s, --su, --set-user Set environment variables to reflect USER\n" +"-l, --login Really log in as USER\n" +" [Default is " +#if DEFAULT_LOGIN_STYLE == l_preserve + "preserve-environment" +#elif DEFAULT_LOGIN_STYLE == l_setuser + "set-user" +#elif DEFAULT_LOGIN_STYLE == l_login + "login" +#else + "poorly configured" +#endif +"]\n\n" +"-g GROUP, --group=GROUP Set primary group-id to be GROUP\n" +#ifdef HAVE_SETGROUPS +"-k, --keep-groups Keep your current set of groups\n" +"-m, --merge-groups Merge the lists of groups\n" +"-r, --replace-groups Replace the list of groups\n" +#endif +"\n" +"-c CMD, --command=CMD Run the (Bourne) shell command CMD\n" +#ifndef NONETWORK +"\n" +"-d, --daemon Start a daemon\n" +"-n, --nofork In daemon mode, don't fork into background\n" +"-p PORT, --port=PORT In daemon mode, listen on PORT\n" +"-f FILE, --config-file=FILE In daemon mode, read config from FILE\n" +#endif + ); +#ifndef NTRACE + bc__write(fp, "\n"); + if (!suid) { + bc__write(fp, +"-I USER, --impersonate=USER Claim to be USER when asking the server\n"); + } + bc__write(fp, +"-T FILE, --trace=FILE Dump trace information to FILE (boring)\n" +"-L OPTS, --trace-level=OPTS Set level of tracing information\n"); #endif -); } /* --- @main@ --- * @@ -233,18 +515,29 @@ int main(int argc, char *argv[]) char *cmd = 0; /* Shell command to execute */ char *binary = "/bin/sh"; /* Default binary to execute */ char **env = environ; /* Default environment to pass */ - char **todo; /* Pointer to argument list */ + char **todo = 0; /* Pointer to argument list */ + char *who = 0; /* Who we're meant to become */ struct passwd *from_pw = 0; /* User we are right now */ struct passwd *to_pw = 0; /* User we want to become */ /* --- Become server setup parameters --- */ +#ifndef NONETWORK char *conffile = file_RULES; /* Default config file for daemon */ - int port = -1; /* Default port for daemon */ + int port = 0; /* Default port for daemon */ +#endif /* --- Miscellanous shared variables --- */ unsigned flags = 0; /* Various useful flags */ + int style = DEFAULT_LOGIN_STYLE; /* Login style */ + gid_t group = -1; /* Default group to set */ + int gstyle = g_unset; /* No group style set yet */ + +#ifdef HAVE_SETGROUPS + gid_t groups[NGROUPS_MAX]; /* Set of groups */ + int ngroups; /* Number of groups in the set */ +#endif /* --- Default argument list executes a shell command --- */ @@ -255,25 +548,15 @@ int main(int argc, char *argv[]) 0 /* Terminator */ }; - /* --- Login request replaces the environment with this --- */ - - static char *mangled_env[] = { - "PATH=/bin:/usr/bin", /* Default `clean' path*/ - 0, /* User's name (`USER') */ - 0, /* User's name (`LOGNAME') */ - 0, /* Home directory (`HOME') */ - 0 /* Terminator */ - }; - /* --- Definitions for the various flags --- */ - enum { - f_daemon = 1, /* Start up in daemon mode */ - f_duff = 2, /* Fault in arguments */ - f_login = 4, /* Execute as a login shell */ - f_dummy = 8, /* Don't actually do anything */ - f_setuid = 16 /* We're running setuid */ - }; +#define f_daemon 1u /* Start up in daemon mode */ +#define f_duff 2u /* Fault in arguments */ +#define f_shell 4u /* Run a default shell */ +#define f_dummy 8u /* Don't actually do anything */ +#define f_setuid 16u /* We're running setuid */ +#define f_havegroup 32u /* Set a default group */ +#define f_nofork 64u /* Don't fork into background */ /* --- Set up the program name --- */ @@ -282,36 +565,110 @@ int main(int argc, char *argv[]) if (getuid() != geteuid()) flags |= f_setuid; + /* --- Make sure standard file descriptors are open --- */ + + { + int fd; + do { + if ((fd = open("/dev/null", O_RDWR)) < 0) + die(1, "couldn't open /dev/null: %s", strerror(errno)); + } while (fd <= STDERR_FILENO); + close(fd); + } + + /* --- Read the environment into a hashtable --- */ + + { + char **p; + + sym_create(&bc__env); + for (p = environ; *p; p++) + bc__putenv(0, *p, 0, 0); + } + /* --- Parse some command line arguments --- */ for (;;) { int i; static struct option opts[] = { + + /* --- Asking for help --- */ + { "help", 0, 0, 'h' }, + { "usage", 0, 0, 'u' }, { "version", 0, 0, 'v' }, + + /* --- Login style options --- */ + + { "preserve-environment", 0, 0, 'e' }, + { "su", 0, 0, 's' }, + { "set-user", 0, 0, 's' }, { "login", 0, 0, 'l' }, - { "command", gFlag_argReq, 0, 'c' }, + + /* --- Group style options --- */ + + { "group", OPTF_ARGREQ, 0, 'g' }, +#ifdef HAVE_SETGROUPS + { "keep-groups", 0, 0, 'k' }, + { "merge-groups", 0, 0, 'm' }, + { "replace-groups", 0, 0, 'r' }, +#endif + + /* --- Command to run options --- */ + + { "command", OPTF_ARGREQ, 0, 'c' }, + + /* --- Server options --- */ + +#ifndef NONETWORK { "daemon", 0, 0, 'd' }, - { "port", gFlag_argReq, 0, 'p' }, - { "config-file", gFlag_argReq, 0, 'f' }, -#ifdef TRACING - { "impersonate", gFlag_argReq, 0, 'I' }, - { "trace", gFlag_argOpt, 0, 'T' }, - { "trace-level", gFlag_argOpt, 0, 'L' }, + { "nofork", 0, 0, 'n' }, + { "port", OPTF_ARGREQ, 0, 'p' }, + { "config-file", OPTF_ARGREQ, 0, 'f' }, #endif + + /* --- Tracing options --- */ + +#ifndef NTRACE + { "impersonate", OPTF_ARGREQ, 0, 'I' }, + { "trace", OPTF_ARGOPT, 0, 'T' }, + { "trace-level", OPTF_ARGOPT, 0, 'L' }, +#endif + { 0, 0, 0, 0 } }; - i = mdwopt(argc, argv, "+hvlc:p:df:", opts, 0, 0, 0); + i = mdwopt(argc, argv, + "-" /* Return non-options as options */ + "huv" /* Asking for help */ + "esl" /* Login style options */ +#ifdef HAVE_SETGROUPS + "g:kmr" /* Group style options */ +#else + "g:" /* Group (without @setgroups@) */ +#endif + "c:" /* Command to run options */ +#ifndef NONETWORK + "dnp:f:" /* Server options */ +#endif +#ifndef NTRACE + "I:T::L::" /* Tracing options */ +#endif + , + opts, 0, 0, gFlag_envVar); if (i < 0) - break; + goto done_options; switch (i) { - /* --- Standard help and version options --- */ + /* --- Asking for help --- */ case 'h': - bc__help(stdout); + bc__help(stdout, flags & f_setuid); + exit(0); + break; + case 'u': + bc__usage(stdout); exit(0); break; case 'v': @@ -319,23 +676,71 @@ int main(int argc, char *argv[]) exit(0); break; - /* --- Various simple options --- */ + /* --- Login style options --- */ + case 'e': + style = l_preserve; + break; + case 's': + style = l_setuser; + break; case 'l': - flags |= f_login; + style = l_login; break; + + /* --- Group style options --- */ + + case 'g': + if (isdigit((unsigned char)optarg[0])) + group = atoi(optarg); + else { + struct group *gr = getgrnam(optarg); + if (!gr) + die(1, "unknown group `%s'", optarg); + group = gr->gr_gid; + } + flags |= f_havegroup; + break; + + case 'k': + gstyle = g_keep; + break; + case 'm': + gstyle = g_merge; + break; + case 'r': + gstyle = g_replace; + break; + + /* --- Command to run options --- */ + case 'c': cmd = optarg; break; + + /* --- Server options --- */ + +#ifndef NONETWORK case 'p': - port = atoi(optarg); + if (isdigit((unsigned char)optarg[0])) + port = htons(atoi(optarg)); + else { + struct servent *s = getservbyname(optarg, "udp"); + if (!s) + die(1, "unknown service name `%s'", optarg); + port = s->s_port; + } break; case 'd': flags |= f_daemon; break; + case 'n': + flags |= f_nofork; + break; case 'f': conffile = optarg; break; +#endif /* --- Pretend to be a different user --- * * @@ -343,7 +748,8 @@ int main(int argc, char *argv[]) * allow it if we're running setuid. Disable the actual login anyway. */ -#ifdef TRACING +#ifndef NTRACE + case 'I': if (flags & f_setuid) moan("shan't allow impersonation while running setuid"); @@ -354,12 +760,13 @@ int main(int argc, char *argv[]) else pw = getpwnam(optarg); if (!pw) - die("can't impersonate unknown user `%s'", optarg); + die(1, "can't impersonate unknown user `%s'", optarg); from_pw = userdb_copyUser(pw); rq.from = from_pw->pw_uid; flags |= f_dummy; } break; + #endif /* --- Tracing support --- * @@ -368,7 +775,7 @@ int main(int argc, char *argv[]) * to! */ -#ifdef TRACING +#ifndef TRACE case 'T': { FILE *fp; @@ -376,16 +783,31 @@ int main(int argc, char *argv[]) if (optarg == 0 || strcmp(optarg, "-") == 0) fp = stdout; else { - if ((flags & f_setuid) && access(optarg, W_OK)) { - die("no write permission for trace file file `%s': %s", - optarg, strerror(errno)); + uid_t eu = geteuid(), ru = getuid(); + +#ifdef HAVE_SETREUID + if (setreuid(eu, ru)) +#else + if (seteuid(ru)) +#endif + { + die(1, "couldn't temporarily give up privileges: %s", + strerror(errno)); } + if ((fp = fopen(optarg, "w")) == 0) { - die("couldn't open trace file `%s' for writing: %s", + die(1, "couldn't open trace file `%s' for writing: %s", optarg, strerror(errno)); } + +#ifdef HAVE_SETREUID + if (setreuid(ru, eu)) +#else + if (seteuid(eu)) +#endif + die(1, "couldn't regain privileges: %s", strerror(errno)); } - traceon(fp, TRACE_DFL); + trace_on(fp, TRACE_DFL); trace(TRACE_MISC, "become: tracing enabled"); } break; @@ -393,84 +815,84 @@ int main(int argc, char *argv[]) /* --- Setting trace levels --- */ -#ifdef TRACING +#ifndef NTRACE case 'L': { - int sense = 1; - unsigned int lvl = 0, l; - const char *p = optarg; /* --- Table of tracing facilities --- */ - typedef struct tr { - char ch; - unsigned int l; - const char *help; - } tr; - - static tr lvltbl[] = { + static trace_opt lvltbl[] = { { 'm', TRACE_MISC, "miscellaneous messages" }, { 's', TRACE_SETUP, "building the request block" }, - { 'd', TRACE_DAEMON, "server process" }, { 'r', TRACE_RULE, "ruleset scanning" }, { 'c', TRACE_CHECK, "request checking" }, +#ifndef NONETWORK + { 'd', TRACE_DAEMON, "server process" }, { 'l', TRACE_CLIENT, "client process" }, { 'R', TRACE_RAND, "random number generator" }, { 'C', TRACE_CRYPTO, "cryptographic processing of requests" }, +#endif { 'y', TRACE_YACC, "parsing configuration file" }, { 'D', TRACE_DFL, "default tracing options" }, { 'A', TRACE_ALL, "all tracing options" }, { 0, 0, 0 } }; - tr *tp; /* --- Output some help if there's no arguemnt --- */ - if (!optarg) { - bc__banner(stdout); - bc__write(stdout, - "\n" - "Tracing options:\n" - "\n"); - for (tp = lvltbl; tp->l; tp++) { - if ((flags & f_setuid) == 0 || tp->l & ~TRACE_PRIV) - printf("%c -- %s\n", tp->ch, tp->help); - } - bc__write(stdout, -"\n" -"Also, `+' and `-' options are recognised to turn on and off vavrious\n" -"tracing options. For example, `A-r' enables everything except ruleset\n" -"tracing, and `A-D+c' is everything except the defaults, but with request\n" -"check tracing.\n" -); - exit(0); - } + trace_level(traceopt(lvltbl, optarg, TRACE_DFL, + (flags & f_setuid) ? TRACE_PRIV : 0)); + } break; + +#endif + + /* --- Something that wasn't an option --- * + * + * The following nasties are supported: + * + * * NAME=VALUE -- preserve NAME, and give it a VALUE + * * NAME= -- preserve NAME, and give it an empty value + * * NAME- -- delete NAME + * * NAME! -- preserve NAME with existing value + * + * Anything else is either the user name (which is OK) or the start of + * the command (in which case I stop and read the rest of the command). + */ + + case 0: { + size_t sz = strcspn(optarg, "=-!"); + sym_env *e; - while (*p) { - if (*p == '+') - sense = 1; - else if (*p == '-') - sense = 0; - else { - for (tp = lvltbl; tp->l && *p != tp->ch; tp++) - ; - l = tp->l; - if (flags & f_setuid) - l &= ~TRACE_PRIV; - if (l) - lvl = sense ? (lvl | l) : (lvl & ~l); - else - moan("unknown trace option `%c'", *p); + /* --- None of the above --- */ + + if (optarg[sz] == 0 || (optarg[sz] != '=' && optarg[sz + 1] != 0)) { + if (!who) { + who = optarg; + break; + } else { + optind--; + goto done_options; } - p++; } - tracesetlvl(lvl); - yydebug = ((lvl & TRACE_YACC) != 0); + /* --- Do the appropriate thing --- */ + + switch (optarg[sz]) { + case '=': + bc__putenv(0, optarg, envFlag_preserve, 1); + break; + case '-': + optarg[sz] = 0; + bc__putenv(optarg, 0, 0, 1); + break; + case '!': + optarg[sz] = 0; + if ((e = sym_find(&bc__env, optarg, -1, 0, 0)) != 0) + e->f |= envFlag_preserve; + break; + } } break; -#endif - /* --- Something I didn't understand has occurred --- */ case '?': @@ -478,6 +900,8 @@ int main(int argc, char *argv[]) break; } } + +done_options: if (flags & f_duff) { bc__usage(stderr); exit(1); @@ -485,11 +909,13 @@ int main(int argc, char *argv[]) /* --- Switch to daemon mode if requested --- */ +#ifndef NONETWORK if (flags & f_daemon) { T( trace(TRACE_MISC, "become: daemon mode requested"); ) - daemon_init(conffile, port); + daemon_init(conffile, port, (flags & f_nofork) ? df_nofork : 0); exit(0); } +#endif /* --- Open a syslog --- */ @@ -498,21 +924,19 @@ int main(int argc, char *argv[]) /* --- Pick out the uid --- */ { - const char *u; struct passwd *pw; - if (optind >= argc) { + if (!who) { bc__usage(stderr); exit(1); } - u = argv[optind++]; - if (isdigit((unsigned char)u[0])) - pw = getpwuid(atoi(u)); + if (isdigit((unsigned char)who[0])) + pw = getpwuid(atoi(who)); else - pw = getpwnam(u); + pw = getpwnam(who); if (!pw) - die("unknown user `%s'", u); + die(1, "unknown user `%s'", who); to_pw = userdb_copyUser(pw); rq.to = pw->pw_uid; } @@ -525,7 +949,7 @@ int main(int argc, char *argv[]) rq.from = getuid(); pw = getpwuid(rq.from); if (!pw) - die("who are you? (can't find user %li)", (long)rq.from); + die(1, "who are you? (can't find user %li)", (long)rq.from); from_pw = userdb_copyUser(pw); } @@ -536,8 +960,133 @@ int main(int argc, char *argv[]) struct hostent *he; uname(&u); if ((he = gethostbyname(u.nodename)) == 0) - die("who am I? (can't resolve `%s')", u.nodename); - memcpy(&rq.host, he->h_addr, sizeof(struct in_addr)); + die(1, "who am I? (can't resolve `%s')", u.nodename); + memcpy(&rq.host, he->h_addr, sizeof(rq.host)); + } + + /* --- Fiddle with group ownerships a bit --- */ + + { +#ifdef HAVE_SETGROUPS + gid_t from_gr[NGROUPS_MAX], to_gr[NGROUPS_MAX]; + int n_fgr, n_tgr; +#endif + + /* --- Set the default login group, if there is one --- */ + + if (~flags & f_havegroup) + group = (style == l_preserve) ? from_pw->pw_gid : to_pw->pw_gid; + +#ifndef HAVE_SETGROUPS + + /* --- Check that it's valid --- */ + + if (group != from_pw->pw_gid && group != to_pw->pw_gid) + die(1, "invalid default group"); + +#else + + /* --- Set the default group style --- */ + + if (gstyle == g_unset) + gstyle = (style == l_login) ? g_replace : g_merge; + + /* --- Read in my current set of groups --- */ + + n_fgr = getgroups(NGROUPS_MAX, from_gr); + + /* --- Now read the groups for the target user --- * + * + * Do this even if I'm using the @g_keep@ style -- I'll use it for + * checking that @group@ is valid. + */ + + { + struct group *gr; + char **p; + + n_tgr = 0; + to_gr[n_tgr++] = to_pw->pw_gid; + + setgrent(); + while ((gr = getgrent()) != 0) { + if (gr->gr_gid == to_gr[0]) + continue; + for (p = gr->gr_mem; *p; p++) { + if (strcmp(to_pw->pw_name, *p) == 0) { + to_gr[n_tgr++] = gr->gr_gid; + if (n_tgr >= NGROUPS_MAX) + goto done_groups; + break; + } + } + } + + done_groups: + endgrent(); + } + + /* --- Check that @group@ is reasonable --- */ + + { + int i; + + if (group == getgid() || group == from_pw->pw_gid) + goto group_ok; + for (i = 0; i < n_fgr; i++) { + if (group == from_gr[i]) + goto group_ok; + } + for (i = 0; i < n_tgr; i++) { + if (group == to_gr[i]) + goto group_ok; + } + die(1, "invalid default group"); + group_ok:; + } + + /* --- Phew. Now comes the hard bit --- */ + + { + gid_t ga[4]; + int i; + + i = 0; + ga[i++] = group; + if (gstyle & g_keep) { + ga[i++] = getgid(); + ga[i++] = from_pw->pw_gid; + } + if (gstyle & g_replace) + ga[i++] = to_pw->pw_gid; + + /* --- Style authorities will become apoplectic if shown this --- * + * + * As far as I can see, it's the neatest way of writing it. + */ + + ngroups = 0; + (void)(bc__addGroups(groups, &ngroups, ga, i) || + ((gstyle & g_keep) && + bc__addGroups(groups, &ngroups, from_gr, n_fgr)) || + ((gstyle & g_replace) && + bc__addGroups(groups, &ngroups, to_gr, n_tgr))); + } + +#endif + + /* --- Trace the results of all this --- */ + + T( trace(TRACE_SETUP, "setup: default group == %i", (int)group); ) + +#ifdef HAVE_SETGROUPS + IF_TRACING(TRACE_SETUP, { + int i; + + for (i = 1; i < ngroups; i++) + trace(TRACE_SETUP, "setup: subsidiary group %i", (int)groups[i]); + }) +#endif } /* --- Shell commands are easy --- */ @@ -554,38 +1103,249 @@ int main(int argc, char *argv[]) binary = todo[0]; } - /* --- A login request needs a little bit of work --- */ + else { + flags |= f_shell; - else if (flags & f_login) { - const char *p = strrchr(to_pw->pw_shell, '/'); - if (p) - p++; - else - p = to_pw->pw_shell; - shell[0] = xmalloc(strlen(p) + 2); - shell[0][0] = '-'; - strcpy(shell[0] + 1, p); - shell[1] = 0; - todo = shell; - binary = to_pw->pw_shell; - } + switch (style) { - /* --- An unadorned becoming requires little work --- */ + /* --- An unadorned becoming requires little work --- */ - else { - shell[0] = from_pw->pw_shell; - shell[1] = 0; - todo = shell; - binary = todo[0]; + case l_preserve: + shell[0] = getenv("SHELL"); + if (!shell[0]) + shell[0] = from_pw->pw_shell; + shell[1] = 0; + todo = shell; + binary = todo[0]; + break; + + /* --- An su-like login needs slightly less effort --- */ + + case l_setuser: + shell[0] = to_pw->pw_shell; + shell[1] = 0; + todo = shell; + binary = todo[0]; + break; + + /* --- A login request needs a little bit of work --- */ + + case l_login: { + const char *p = strrchr(to_pw->pw_shell, '/'); + + if (p) + p++; + else + p = to_pw->pw_shell; + shell[0] = xmalloc(strlen(p) + 2); + shell[0][0] = '-'; + strcpy(shell[0] + 1, p); + shell[1] = 0; + todo = shell; + binary = to_pw->pw_shell; + } break; + } } - /* --- Mangle the environment if login flag given --- */ + /* --- Mangle the environment --- * + * + * This keeps getting more complicated all the time. (How true. Now I've + * got all sorts of nasty environment mangling to do.) + * + * The environment stuff now happens in seven phases: + * + * 1. Mark very special variables to be preserved. Currently only TERM + * and DISPLAY are treated in this way. + * + * 2. Set and preserve Become's own environment variables. + * + * 3. Set and preserve the user identity variables (USER, LOGNAME, HOME, + * SHELL and MAIL) if we're being `su'-like or `login'-like. + * + * 4. If we're preserving the environment or being `su'-like, process the + * PATH variable a little. Otherwise reset it to something + * appropriate. + * + * 5. If we're being `login'-like, expunge all unpreserved variables. + * + * 6. Expunge any security-critical variables. + * + * 7. Build a new environment table to pass to child processes. + */ + + { + /* --- Variables to be preserved always --- * + * + * A user can explicitly expunge a variable in this list, in which case + * we never get to see it here. + */ + + static char *preserve[] = { + "TERM", "DISPLAY", "TZ", 0 + }; + + /* --- Variables to be expunged --- * + * + * Any environment string which has one of the following as a prefix will + * be expunged from the environment passed to the called process. The + * first line lists variables which have been used to list search paths + * for shared libraries: by manipulating these, an attacker could replace + * a standard library with one of his own. The second line lists other + * well-known dangerous environment variables. + */ + + static char *banned[] = { + "-LD_", "SHLIB_PATH", "LIBPATH", "-_RLD_", + "IFS", "ENV", "BASH_ENV", "KRB_CONF", + 0 + }; + + /* --- Other useful variables --- */ + + sym_env *e; + char *p, *q, *r; + char **pp, **qq; + size_t sz; + unsigned f; + sym_iter i; + + /* --- Stage one. Preserve display-specific variables --- */ + + for (pp = preserve; *pp; pp++) { + if ((e = sym_find(&bc__env, *pp, -1, 0, 0)) != 0) + e->f |= envFlag_preserve; + } + + /* --- Stage two. Set Become's own variables --- */ + + e = sym_find(&bc__env, "BECOME_ORIGINAL_USER", -1, sizeof(*e), &f); + if (!f) + bc__setenv(e, from_pw->pw_name); + e->f |= envFlag_preserve; + + e = sym_find(&bc__env, "BECOME_ORIGINAL_HOME", -1, sizeof(*e), &f); + if (!f) + bc__setenv(e, from_pw->pw_dir); + e->f |= envFlag_preserve; + + bc__putenv("BECOME_OLD_USER", from_pw->pw_name, envFlag_preserve, 0); + bc__putenv("BECOME_OLD_HOME", from_pw->pw_dir, envFlag_preserve, 0); + bc__putenv("BECOME_USER", to_pw->pw_name, envFlag_preserve, 0); + bc__putenv("BECOME_HOME", to_pw->pw_dir, envFlag_preserve, 0); + + /* --- Stage three. Set user identity --- */ + + switch (style) { + case l_login: { + static char *maildirs[] = { + "/var/spool/mail", "/var/mail", + "/usr/spool/mail", "/usr/mail", + 0 + }; + struct stat s; + char b[128]; + + for (pp = maildirs; *pp; pp++) { + if (stat(*pp, &s) == 0 && S_ISDIR(s.st_mode)) { + sprintf(b, "%s/%s", *pp, to_pw->pw_name); + bc__putenv("MAIL", b, envFlag_preserve, 0); + break; + } + } + } /* Fall through */ + + case l_setuser: + bc__putenv("USER", to_pw->pw_name, envFlag_preserve, 0); + bc__putenv("LOGNAME", to_pw->pw_name, envFlag_preserve, 0); + bc__putenv("HOME", to_pw->pw_dir, envFlag_preserve, 0); + bc__putenv("SHELL", to_pw->pw_shell, envFlag_preserve, 0); + break; + } - if (flags & f_login) { - env = mangled_env; - env[1] = bc__makeEnv("USER", to_pw->pw_name); - env[2] = bc__makeEnv("LOGNAME", to_pw->pw_name); - env[3] = bc__makeEnv("HOME", to_pw->pw_dir); + /* --- Stage four. Set the user's PATH properly --- */ + + { + /* --- Find an existing path --- * + * + * If there's no path, or this is a login, then set a default path, + * unless we're meant to preserve the existing one. Whew! + */ + + e = sym_find(&bc__env, "PATH", -1, sizeof(*e), &f); + + if (!f || (style == l_login && ~e->f & envFlag_preserve)) { + bc__putenv("PATH", + rq.to ? "/usr/bin:/bin" : "/usr/bin:/usr/sbin:/bin:/sbin", + envFlag_preserve, 0); + } else { + + /* --- Find the string --- */ + + e->f = envFlag_preserve; + p = strchr(e->val, '=') + 1; + r = e->val; + + /* --- Write the new version to a dynamically allocated buffer --- */ + + e->val = xmalloc(4 + 1 + strlen(p) + 1); + strcpy(e->val, "PATH="); + q = e->val + 5; + + for (p = strtok(p, ":"); p; p = strtok(0, ":")) { + if (p[0] != '/') + continue; + while (*p) + *q++ = *p++; + *q++ = ':'; + } + q[-1] = 0; + + /* --- Done! --- */ + + free(r); + } + } + + /* --- Stages five and six. Expunge variables and count numbers --- * + * + * Folded together, so I only need one pass through the table. Also + * count the number of variables needed at this time. + */ + + sz = 0; + + for (sym_mkiter(&i, &bc__env); (e = sym_next(&i)) != 0; ) { + + /* --- Login style expunges all unpreserved variables --- */ + + if (style == l_login && ~e->f & envFlag_preserve) + goto expunge; + + /* --- Otherwise just check the name against the list --- */ + + for (pp = banned; *pp; pp++) { + if (**pp == '-') { + p = *pp + 1; + if (strncmp(e->_base.name, p, strlen(p)) == 0) + goto expunge; + } else if (strcmp(e->_base.name, *pp) == 0) + goto expunge; + } + + sz++; + continue; + + expunge: + sym_remove(&bc__env, e); + } + + /* --- Stage seven. Build the new environment block --- */ + + env = qq = xmalloc((sz + 1) * sizeof(*qq)); + + for (sym_mkiter(&i, &bc__env); (e = sym_next(&i)) != 0; ) + *qq++ = e->val; + *qq++ = 0; } /* --- Trace the command --- */ @@ -612,24 +1372,36 @@ int main(int argc, char *argv[]) p = "/bin:/usr/bin"; path = xstrdup(p); - for (p = strtok(path, ":"); (p = strtok(0, ":")) != 0; ) { + for (p = strtok(path, ":"); p; p = strtok(0, ":")) { - /* --- SECURITY: check length of string before copying --- */ + /* --- Check length of string before copying --- */ if (strlen(p) + strlen(binary) + 2 > sizeof(rq.cmd)) continue; - /* --- Now build the pathname and check it --- */ + /* --- Now build the pathname and check it --- * + * + * Issue: user can take advantage of these privileges to decide whether + * a program with a given name exists. I'm not sure that's + * particularly significant: it only works on regular files with + * execute permissions, and if you're relying on the names of these + * being secret to keep your security up, then you're doing something + * deeply wrong anyway. On the other hand, it's useful to allow people + * to be able to execute programs and scripts which they wouldn't + * otherwise have access to. [This problem was brought up on + * Bugtraq, as a complaint against sudo.] + */ - sprintf(rq.cmd, "%s/%s", p, todo[0]); + if (!*p) p = "."; + sprintf(rq.cmd, "%s/%s", p, binary); if (stat(rq.cmd, &st) == 0 && /* Check it exists */ st.st_mode & 0111 && /* Check it's executable */ - (st.st_mode & S_IFMT) == S_IFREG) /* Check it's a file */ + S_ISREG(st.st_mode)) /* Check it's a file */ break; } if (!p) - die("couldn't find `%s' in path", todo[0]); + die(1, "couldn't find `%s' in path", todo[0]); binary = rq.cmd; free(path); } @@ -648,7 +1420,7 @@ int main(int argc, char *argv[]) q = binary; if (*q != '/') { if (!getcwd(b, sizeof(b))) - die("couldn't read current directory: %s", strerror(errno)); + die(1, "couldn't read current directory: %s", strerror(errno)); p += strlen(p); *p++ = '/'; } @@ -657,14 +1429,14 @@ int main(int argc, char *argv[]) while (*q) { - /* --- SECURITY: check for buffer overflows here --- * + /* --- Check for buffer overflows here --- * * * I write at most one byte per iteration so this is OK. Remember to * allow one for the null byte. */ if (p >= b + sizeof(b) - 1) - die("internal error: buffer overflow while canonifying path"); + die(1, "internal error: buffer overflow while canonifying path"); /* --- Reduce multiple slashes to just one --- */ @@ -715,17 +1487,20 @@ int main(int argc, char *argv[]) * * If the user is already what she wants to be, then print a warning. * Then, if I was just going to spawn a shell, quit, to reduce user - * confusion. Otherwise, do what was wanted anyway. + * confusion. Otherwise, do what was wanted anyway. Also, don't bother + * checking if we're already root -- root can do anything anyway, and at + * least this way we get some logging done, and offer a more friendly + * front-end. */ if (rq.from == rq.to) { moan("you already are `%s'!", to_pw->pw_name); - if (!cmd && todo == shell) { + if (flags & f_shell) { moan("(to prevent confusion, I'm not spawning a shell)"); exit(0); } } else { - int a = check(&rq); + int a = (rq.from == 0) || check(&rq); syslog(LOG_INFO, "permission %s for %s to become %s to run `%s'", @@ -733,7 +1508,7 @@ int main(int argc, char *argv[]) rq.cmd); if (!a) - die("permission denied"); + die(1, "permission denied"); } /* --- Now do the job --- */ @@ -743,13 +1518,34 @@ int main(int argc, char *argv[]) if (flags & f_dummy) { puts("permission granted"); return (0); - } else { - if (setuid(rq.to) == -1) - die("couldn't set uid: %s", strerror(errno)); - execve(rq.cmd, todo, env); - die("couldn't exec `%s': %s", rq.cmd, strerror(errno)); - return (127); } + +#ifdef HAVE_SETGROUPS + if (setgroups(ngroups, groups) < 0) + die(1, "couldn't set groups: %s", strerror(errno)); +#endif + + if (setgid(group) < 0) + die(1, "couldn't set default group: %s", strerror(errno)); + if (setuid(rq.to) < 0) + die(1, "couldn't set uid: %s", strerror(errno)); + + /* --- If this was a login, change current directory --- */ + + if ((flags & f_shell) && + style == l_login && + chdir(to_pw->pw_dir) < 0) { + moan("couldn't change directory to `%s': %s", + to_pw->pw_dir, strerror(errno)); + } + + /* --- Finally, call the program --- */ + + fflush(0); + closelog(); + execve(rq.cmd, todo, env); + die(1, "couldn't exec `%s': %s", rq.cmd, strerror(errno)); + return (127); } /*----- That's all, folks -------------------------------------------------*/