/* -*-c-*-
*
- * $Id: become.c,v 1.1 1997/07/21 13:47:54 mdw Exp $
+ * $Id$
*
* Main code for `become'
*
- * (c) 1997 EBI
+ * (c) 1998 EBI
*/
-/*----- Licencing notice --------------------------------------------------*
+/*----- Licensing notice --------------------------------------------------*
*
* This file is part of `become'
*
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
- * along with `become'; if not, write to the Free Software
- * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
- */
-
-/*----- Revision history --------------------------------------------------*
- *
- * $Log: become.c,v $
- * Revision 1.1 1997/07/21 13:47:54 mdw
- * Initial revision
- *
+ * along with `become'; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
/*----- Header files ------------------------------------------------------*/
#include <ctype.h>
#include <errno.h>
+#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <time.h>
/* --- Unix headers --- */
#include <arpa/inet.h>
#include <netdb.h>
+#include <grp.h>
#include <pwd.h>
#include <syslog.h>
#include <unistd.h>
+#include <fcntl.h>
+
+extern char **environ;
+
+/* --- mLib --- */
+
+#include <mLib/alloc.h>
+#include <mLib/mdwopt.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+#include <mLib/sym.h>
+#include <mLib/trace.h>
/* --- Local headers --- */
#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 ---------------------------------------------------------*/
/* --- 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.
*/
}
}
+/* --- @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
{
bc__write(fp,
"Usage: \n"
- " $ -c <shell-command> <user>\n"
- " $ <user> [<command> [<arguments>]...]\n"
- " $ -d [-p <port>] [-f <config-file>]\n");
+ " $ -c SHELL-COMMAND USER\n"
+ " $ [ENV-VAR] USER [COMMAND [ARGUMENTS]...]\n"
+#ifndef NONETWORK
+ " $ -d [-p PORT] [-f CONFIG-FILE]\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);
"\n"
"Options available are:\n"
"\n"
-"-h, --help Display this help text\n"
-"-v, --version Display the version number of this copy of $\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"
-"--yacc-debug Dump lots of parser diagnostics (boring)\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@ --- *
int main(int argc, char *argv[])
{
- char *cmd = 0;
- static char *shell[] = { "/bin/sh", "-c", 0, 0 };
- char **todo;
- request rq;
- char buff[CMDLEN_MAX];
- char *conffile = file_RULES;
- int port = -1;
-
- enum {
- f_daemon = 1,
- f_duff = 2
+ /* --- Request block setup parameters --- */
+
+ request rq; /* Request buffer to build */
+ char *cmd = 0; /* Shell command to execute */
+ char *binary = "/bin/sh"; /* Default binary to execute */
+ char **env = environ; /* Default environment to pass */
+ 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 = 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 --- */
+
+ static char *shell[] = {
+ "/bin/sh", /* Bourne shell */
+ "-c", /* Read from command line */
+ 0, /* Pointer to shell command */
+ 0 /* Terminator */
};
- unsigned flags = 0;
+ /* --- Definitions for the various flags --- */
+
+#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 --- */
ego(argv[0]);
+ clock();
+ 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;
- struct option opts[] = {
+ static struct option opts[] = {
+
+ /* --- Asking for help --- */
+
{ "help", 0, 0, 'h' },
+ { "usage", 0, 0, 'u' },
{ "version", 0, 0, 'v' },
- { "command", gFlag_argReq, 0, 'c' },
+
+ /* --- Login style options --- */
+
+ { "preserve-environment", 0, 0, 'e' },
+ { "su", 0, 0, 's' },
+ { "set-user", 0, 0, 's' },
+ { "login", 0, 0, 'l' },
+
+ /* --- 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' },
- { "yacc-debug", 0, 0, 'Y' },
+ { "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, "hvc: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) {
+
+ /* --- 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':
bc__banner(stdout);
exit(0);
break;
+
+ /* --- Login style options --- */
+
+ case 'e':
+ style = l_preserve;
+ break;
+ case 's':
+ style = l_setuser;
+ break;
+ case 'l':
+ 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;
- case 'Y':
- if (getuid() == geteuid())
- yydebug = 1;
- else
- moan("won't set debugging mode when running setuid");
+#endif
+
+ /* --- Pretend to be a different user --- *
+ *
+ * There are all sorts of nasty implications for this option. Don't
+ * allow it if we're running setuid. Disable the actual login anyway.
+ */
+
+#ifndef NTRACE
+
+ case 'I':
+ if (flags & f_setuid)
+ moan("shan't allow impersonation while running setuid");
+ else {
+ struct passwd *pw;
+ if (isdigit((unsigned char)optarg[0]))
+ pw = getpwuid(atoi(optarg));
+ else
+ pw = getpwnam(optarg);
+ if (!pw)
+ 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 --- *
+ *
+ * Be careful not to zap a file I wouldn't normally be allowed to write
+ * to!
+ */
+
+#ifndef TRACE
+
+ case 'T': {
+ FILE *fp;
+
+ if (optarg == 0 || strcmp(optarg, "-") == 0)
+ fp = stdout;
+ else {
+ 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(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));
+ }
+ trace_on(fp, TRACE_DFL);
+ trace(TRACE_MISC, "become: tracing enabled");
+ } break;
+
+#endif
+
+ /* --- Setting trace levels --- */
+
+#ifndef NTRACE
+
+ case 'L': {
+
+ /* --- Table of tracing facilities --- */
+
+ static trace_opt lvltbl[] = {
+ { 'm', TRACE_MISC, "miscellaneous messages" },
+ { 's', TRACE_SETUP, "building the request block" },
+ { '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 }
+ };
+
+ /* --- Output some help if there's no arguemnt --- */
+
+ 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;
+
+ /* --- None of the above --- */
+
+ if (optarg[sz] == 0 || (optarg[sz] != '=' && optarg[sz + 1] != 0)) {
+ if (!who) {
+ who = optarg;
+ break;
+ } else {
+ optind--;
+ goto done_options;
+ }
+ }
+
+ /* --- 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;
+
+ /* --- Something I didn't understand has occurred --- */
+
case '?':
flags |= f_duff;
break;
}
}
+
+done_options:
if (flags & f_duff) {
bc__usage(stderr);
exit(1);
/* --- Switch to daemon mode if requested --- */
+#ifndef NONETWORK
if (flags & f_daemon) {
- daemon_init(conffile, port);
+ T( trace(TRACE_MISC, "become: daemon mode requested"); )
+ daemon_init(conffile, port, (flags & f_nofork) ? df_nofork : 0);
exit(0);
}
+#endif
/* --- Open a syslog --- */
/* --- Pick out the uid --- */
{
- const char *u;
struct passwd *pw;
- if (optind >= argc) {
+
+ if (!who) {
bc__usage(stderr);
exit(1);
}
- u = argv[optind++];
- pw = getpwnam(u);
- if (!pw && isdigit(u[0]))
- pw = getpwuid(atoi(u));
+
+ if (isdigit((unsigned char)who[0]))
+ pw = getpwuid(atoi(who));
+ else
+ 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;
}
/* --- Fill in the easy bits of the request --- */
- rq.from = getuid();
+ if (!from_pw) {
+ struct passwd *pw;
+
+ rq.from = getuid();
+ pw = getpwuid(rq.from);
+ if (!pw)
+ die(1, "who are you? (can't find user %li)", (long)rq.from);
+ from_pw = userdb_copyUser(pw);
+ }
/* --- Find the local host address --- */
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
}
- /* --- Figure out what command to use --- */
+ /* --- Shell commands are easy --- */
if (cmd) {
shell[2] = cmd;
todo = shell;
- } else if (optind < argc) {
+ }
+
+ /* --- A command given on the command line isn't too hard --- */
+
+ else if (optind < argc) {
todo = argv + optind;
- } else {
- struct passwd *pw = getpwuid(rq.from);
- if (!pw)
- die("who are you? (can't find uid %li in database)", (long)rq.from);
- shell[0] = pw->pw_shell;
- shell[1] = 0;
- todo = shell;
+ binary = todo[0];
+ }
+
+ else {
+ flags |= f_shell;
+
+ switch (style) {
+
+ /* --- An unadorned becoming requires little work --- */
+
+ 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 --- *
+ *
+ * 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;
+ }
+
+ /* --- 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 --- */
+
+ IF_TRACING(TRACE_SETUP, {
+ int i;
+
+ trace(TRACE_SETUP, "setup: from user %s to user %s",
+ from_pw->pw_name, to_pw->pw_name);
+ trace(TRACE_SETUP, "setup: binary == `%s'", binary);
+ for (i = 0; todo[i]; i++)
+ trace(TRACE_SETUP, "setup: arg %i == `%s'", i, todo[i]);
+ for (i = 0; env[i]; i++)
+ trace(TRACE_SETUP, "setup: env %i == `%s'", i, env[i]);
+ })
+
/* --- If necessary, resolve the path to the command --- */
- if (!strchr(todo[0], '/')) {
- char *path;
- char *p;
+ if (!strchr(binary, '/')) {
+ char *path, *p;
struct stat st;
- size_t sz;
if ((p = getenv("PATH")) == 0)
p = "/bin:/usr/bin";
- sz = strlen(p) + 1;
- memcpy(path = xmalloc(sz), p, sz);
+ 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(todo[0]) + 2 > sizeof(buff))
+ 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(buff, "%s/%s", p, todo[0]);
- if (stat(buff, &st) == 0 && /* Check it exists */
+ 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]);
- todo[0] = buff;
+ die(1, "couldn't find `%s' in path", todo[0]);
+ binary = rq.cmd;
free(path);
}
+ T( trace(TRACE_SETUP, "setup: resolve binary to `%s'", binary); )
/* --- Canonicalise the path string, if necessary --- */
/* --- Insert current directory name if path not absolute --- */
p = b;
- q = todo[0];
+ 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++ = '/';
}
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("buffer overflow -- bad things happened");
+ die(1, "internal error: buffer overflow while canonifying path");
/* --- Reduce multiple slashes to just one --- */
*p++ = 0;
strcpy(rq.cmd, b);
}
+ T( trace(TRACE_SETUP, "setup: canonify binary to `%s'", rq.cmd); )
- /* --- Run the check --- */
-
- {
- int a = check(&rq);
- char from[16], to[16];
- struct passwd *pw;
-
- if ((pw = getpwuid(rq.from)) != 0)
- sprintf(from, "%.15s", pw->pw_name);
- else
- sprintf(from, "user %lu", (unsigned long)rq.from);
+ /* --- Run the check --- *
+ *
+ * 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. 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 ((pw = getpwuid(rq.to)) != 0)
- sprintf(to, "%.15s", pw->pw_name);
- else
- sprintf(to, "user %lu", (unsigned long)rq.to);
+ if (rq.from == rq.to) {
+ moan("you already are `%s'!", to_pw->pw_name);
+ if (flags & f_shell) {
+ moan("(to prevent confusion, I'm not spawning a shell)");
+ exit(0);
+ }
+ } else {
+ int a = (rq.from == 0) || check(&rq);
syslog(LOG_INFO,
"permission %s for %s to become %s to run `%s'",
- a ? "granted" : "denied", from, to, rq.cmd);
+ a ? "granted" : "denied", from_pw->pw_name, to_pw->pw_name,
+ rq.cmd);
if (!a)
- die("permission denied");
+ die(1, "permission denied");
}
/* --- Now do the job --- */
-#ifdef TEST_RIG
- printf("ok\n");
- return (0);
-#else
- if (setuid(rq.to) == -1 || seteuid(rq.to) == -1)
- die("couldn't set uid: %s", strerror(errno));
- execv(rq.cmd, todo);
- die("couldn't exec `%s': %s", rq.cmd, strerror(errno));
- return (127);
+ T( trace(TRACE_MISC, "become: permission granted"); )
+
+ if (flags & f_dummy) {
+ puts("permission granted");
+ return (0);
+ }
+
+#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 -------------------------------------------------*/