Overhaul of environment handling. Fix daft bug in path search code.
[become] / src / become.c
index c2d8bcf..6f5c236 100644 (file)
@@ -1,13 +1,13 @@
 /* -*-c-*-
  *
- * $Id: become.c,v 1.1 1997/07/21 13:47:54 mdw Exp $
+ * $Id: become.c,v 1.4 1997/08/20 16:15:13 mdw Exp $
  *
  * Main code for `become'
  *
  * (c) 1997 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.
+ * along with `become'; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  */
 
 /*----- Revision history --------------------------------------------------*
  *
  * $Log: become.c,v $
- * Revision 1.1  1997/07/21 13:47:54  mdw
+ * 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.
+ *
+ * Revision 1.2  1997/08/04 10:24:20  mdw
+ * Sources placed under CVS control.
+ *
+ * Revision 1.1  1997/07/21  13:47:54  mdw
  * Initial revision
  *
  */
@@ -43,6 +52,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <time.h>
 
 /* --- Unix headers --- */
 
@@ -60,6 +70,8 @@
 #include <syslog.h>
 #include <unistd.h>
 
+extern char **environ;
+
 /* --- Local headers --- */
 
 #include "become.h"
@@ -72,6 +84,7 @@
 #include "parser.h"
 #include "rule.h"
 #include "utils.h"
+#include "userdb.h"
 
 /*----- Main code ---------------------------------------------------------*/
 
@@ -95,7 +108,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.
    */
 
@@ -143,16 +156,35 @@ static void bc__usage(FILE *fp)
            "   $ -d [-p <port>] [-f <config-file>]\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);
+}
+
 /* --- @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);
@@ -174,11 +206,20 @@ static void bc__help(FILE *fp)
 "\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"
-"--yacc-debug          Dump lots of parser diagnostics (boring)\n");
+"-f, --config-file=FILE        In daemon mode, read config from FILE\n");
+#ifdef TRACING
+  if (!suid) {
+    bc__write(fp,
+"--impersonate=USER    Claim to be USER when asking the server\n");
+  }
+  bc__write(fp,
+"--trace=FILE          Dump trace information to FILE (boring)\n"
+"--trace-level=OPTS    Set level of tracing information\n");
+#endif
 }
 
 /* --- @main@ --- *
@@ -193,53 +234,98 @@ static void bc__help(FILE *fp)
 
 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;
+  /* --- Request block setup parameters --- */
 
-  enum {
-    f_daemon = 1,
-    f_duff = 2
+  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;                         /* Pointer to argument list */
+  struct passwd *from_pw = 0;          /* User we are right now */
+  struct passwd *to_pw = 0;            /* User we want to become */
+
+  /* --- Become server setup parameters --- */
+
+  char *conffile = file_RULES;         /* Default config file for daemon */
+  int port = -1;                       /* Default port for daemon */
+
+  /* --- Miscellanous shared variables --- */
+
+  unsigned flags = 0;                  /* Various useful flags */
+
+  /* --- 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 --- */
+
+  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 */
+  };
 
   /* --- Set up the program name --- */
 
   ego(argv[0]);
+  clock();
+  if (getuid() != geteuid())
+    flags |= f_setuid;
 
   /* --- Parse some command line arguments --- */
 
   for (;;) {
     int i;
-    struct option opts[] = {
+    static struct option opts[] = {
       { "help",                0,              0,      'h' },
+      { "usage",       0,              0,      'U' },
       { "version",     0,              0,      'v' },
+      { "login",       0,              0,      'l' },
       { "command",     gFlag_argReq,   0,      'c' },
       { "daemon",      0,              0,      'd' },
       { "port",                gFlag_argReq,   0,      'p' },
       { "config-file", gFlag_argReq,   0,      'f' },
-      { "yacc-debug",  0,              0,      'Y' },
+#ifdef TRACING
+      { "impersonate", gFlag_argReq,   0,      'I' },
+      { "trace",       gFlag_argOpt,   0,      'T' },
+      { "trace-level", gFlag_argOpt,   0,      'L' },
+#endif
       { 0,             0,              0,      0 }
     };
 
-    i = mdwopt(argc, argv, "hvc:p:df:", opts, 0, 0, 0);
+    i = mdwopt(argc, argv, "+hvlc:p:df:", opts, 0, 0, 0);
     if (i < 0)
       break;
 
     switch (i) {
+
+      /* --- Standard help and version options --- */
+
       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;
+
+      /* --- Various simple options --- */
+
+      case 'l':
+       flags |= f_login;
+       break;
       case 'c':
        cmd = optarg;
        break;
@@ -252,12 +338,145 @@ int main(int argc, char *argv[])
       case 'f':
        conffile = optarg;
        break;
-      case 'Y':
-       if (getuid() == geteuid())
-         yydebug = 1;
-       else
-         moan("won't set debugging mode when running setuid");
+
+      /* --- 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.
+       */
+
+#ifdef TRACING
+
+      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("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!
+       */
+
+#ifdef TRACING
+
+      case 'T': {
+       FILE *fp;
+
+       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));
+         }
+         if ((fp = fopen(optarg, "w")) == 0) {
+           die("couldn't open trace file `%s' for writing: %s",
+               optarg, strerror(errno));
+         }
+       }
+       traceon(fp, TRACE_DFL);
+       trace(TRACE_MISC, "become: tracing enabled");
+      } break;
+
+#endif
+
+      /* --- Setting trace levels --- */
+
+#ifdef TRACING
+
+      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[] = {
+         { '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" },
+         { 'l', TRACE_CLIENT,  "client process" },
+         { 'R', TRACE_RAND,    "random number generator" },
+         { 'C', TRACE_CRYPTO,  "cryptographic processing of requests" },
+         { '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);
+       }
+
+       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);
+         }
+         p++;
+       }
+
+       tracesetlvl(lvl);
+       yydebug = ((lvl & TRACE_YACC) != 0);
+      } break;
+
+#endif
+
+      /* --- Something I didn't understand has occurred --- */
+
       case '?':
        flags |= f_duff;
        break;
@@ -271,6 +490,7 @@ int main(int argc, char *argv[])
   /* --- Switch to daemon mode if requested --- */
 
   if (flags & f_daemon) {
+    T( trace(TRACE_MISC, "become: daemon mode requested"); )
     daemon_init(conffile, port);
     exit(0);
   }
@@ -284,22 +504,34 @@ int main(int argc, char *argv[])
   {
     const char *u;
     struct passwd *pw;
+
     if (optind >= argc) {
       bc__usage(stderr);
       exit(1);
     }
     u = argv[optind++];
-    pw = getpwnam(u);
-    if (!pw && isdigit(u[0]))
+
+    if (isdigit((unsigned char)u[0]))
       pw = getpwuid(atoi(u));
+    else
+      pw = getpwnam(u);
     if (!pw)
       die("unknown user `%s'", u);
+    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("who are you? (can't find user %li)", (long)rq.from);
+    from_pw = userdb_copyUser(pw);
+  }
 
   /* --- Find the local host address --- */
 
@@ -312,46 +544,177 @@ int main(int argc, char *argv[])
     memcpy(&rq.host, he->h_addr, sizeof(struct in_addr));
   }
 
-  /* --- 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;
+    binary = todo[0];
+  }
+
+  /* --- A login request needs a little bit of work --- */
+
+  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;
+  }
+
+  /* --- An unadorned becoming requires little work --- */
+
+  else {
+    shell[0] = getenv("SHELL");
+    if (!shell[0])
+      shell[0] = from_pw->pw_shell;
+    shell[1] = 0;
+    todo = shell;
+    binary = todo[0];
+  }
+
+  /* --- Mangle the environment --- *
+   *
+   * This keeps getting more complicated all the time.
+   */
+
+  {
+    size_t i, j, b;
+    int pass;
+
+    /* --- Expunge some environment variables --- *
+     *
+     * 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 const char *banned[] = {
+      "LD_", "SHLIB_PATH=", "LIBPATH=", "_RLD_",
+      "IFS=", "ENV=", "BASH_ENV=", "KRB_CONF=",
+      0
+    };
+
+    /* --- Do this in two passes --- *
+     *
+     * The first pass works out how big the environment block needs to be.
+     * The second actually fills it.
+     */
+
+    for (pass = 0; pass < 2; pass++) {
+      i = j = 0;
+
+      if (flags & f_login) {
+
+       /* --- This is a login request --- *
+        *
+        * Erase the existing environment and build a new one.
+        */
+
+       if (!pass)
+         i += 4;
+       else {
+         env[i++] = "PATH=/usr/bin:/bin";
+         env[i++] = bc__makeEnv("USER", to_pw->pw_name);
+         env[i++] = bc__makeEnv("LOGNAME", to_pw->pw_name);
+         env[i++] = bc__makeEnv("HOME", to_pw->pw_dir);
+       }
+      } else {
+
+       /* --- Normal request --- *
+        *
+        * Remove dangerous variables from the list.
+        */
+
+       for (j = 0; environ[j]; j++) {
+         for (b = 0; banned[b]; b++) {
+           if (memcmp(environ[j], banned[b], strlen(banned[b])) == 0)
+             goto skip_var;
+         }
+         if (pass)
+           env[i] = environ[j];
+         i++;
+       skip_var:;
+       }
+      }
+
+      /* --- Now add our own variables --- *
+       *
+       * The following are supplied only to help people construct startup
+       * scripts.  Anyone who relies on them being accurate for
+       * authentication purposes will get exactly what they deserve.
+       */
+
+      if (!pass)
+       i += 4;
+      else {
+       env[i++] = bc__makeEnv("BECOME_OLDUSER", from_pw->pw_name);
+       env[i++] = bc__makeEnv("BECOME_OLDHOME", from_pw->pw_dir);
+       env[i++] = bc__makeEnv("BECOME_USER", to_pw->pw_name);
+       env[i++] = bc__makeEnv("BECOME_HOME", to_pw->pw_dir);
+      }
+
+      /* --- Allocate memory after the first pass is complete --- */
+
+      if (pass)
+       env[i] = 0;
+      i++;
+
+      if (!pass)
+       env = xmalloc(i * sizeof(env[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 --- */
 
-      sprintf(buff, "%s/%s", p, todo[0]);
-      if (stat(buff, &st) == 0 &&      /* Check it exists */
+      sprintf(rq.cmd, "%s/%s", p, todo[0]);
+      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 */
        break;
@@ -359,9 +722,10 @@ int main(int argc, char *argv[])
 
     if (!p)
       die("couldn't find `%s' in path", todo[0]);
-    todo[0] = buff;
+    binary = rq.cmd;
     free(path);
   }
+  T( trace(TRACE_SETUP, "setup: resolve binary to `%s'", binary); )
 
   /* --- Canonicalise the path string, if necessary --- */
 
@@ -373,7 +737,7 @@ int main(int argc, char *argv[])
     /* --- 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));
@@ -385,14 +749,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("buffer overflow -- bad things happened");
+       die("internal error: buffer overflow while canonifying path");
 
       /* --- Reduce multiple slashes to just one --- */
 
@@ -437,27 +801,28 @@ int main(int argc, char *argv[])
     *p++ = 0;
     strcpy(rq.cmd, b);
   }
+  T( trace(TRACE_SETUP, "setup: canonify binary to `%s'", rq.cmd); )
 
-  /* --- Run the check --- */
+  /* --- 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.
+   */
 
-  {
+  if (rq.from == rq.to) {
+    moan("you already are `%s'!", to_pw->pw_name);
+    if (!cmd && todo == shell) {
+      moan("(to prevent confusion, I'm not spawning a shell)");
+      exit(0);
+    }
+  } else {
     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);
-
-    if ((pw = getpwuid(rq.to)) != 0)
-      sprintf(to, "%.15s", pw->pw_name);
-    else
-      sprintf(to, "user %lu", (unsigned long)rq.to);
 
     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");
@@ -465,16 +830,18 @@ int main(int argc, char *argv[])
 
   /* --- 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);
-#endif
+  T( trace(TRACE_MISC, "become: permission granted"); )
+
+  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);
+  }
 }
 
 /*----- That's all, folks -------------------------------------------------*/