/* * File: chrsh.c * Version: 0.6.0 * Author: Aaron D. Gifford * Title: The Chroot Shell: A chroot jail wrapper for ordinary Unix shells * * * Copyright (c) 1998 Aaron D. Gifford. All rights reserved. * * * You may redistribute and use in source or binary form, with or * without modification provided that credit to the author(s) * remains intact and that the appropriate copyright, license, * and/or disclaimers remain intact. * * THIS SOFTWARE IS PROVIDED BY AARON D. GIFFORD ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL AARON D. GIFFORD OR * OTHER CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * * * Thanks to: * The Apache Group for many ideas borrowed from suexec.c * * * BUG FIXES, COMMENTS, and SUGGESTIONS are always WELCOME! * * Please send bug fixes, suggestions, or comments to: * * Aaron D. Gifford * < a g i f f o r d AT i n f o w e s t DOT c o m > * * I may or may not reply. I did this in my own spare time. * I will NOT reply to messages that basically require me * to give technical training or support related to this * software. I just don't have time for that. * * Likewise I may or may not include such fixes in future * releases (IF future releases ever occur). * * As you can tell, this was developed on FreeBSD, so there * are NO cross-platform compatibility features in here at * all! If you get it working, e-mail me a diff -p and * perhaps I'll include it next time. * * And spam gets filed in /dev/null and I flag the company * or product so promoted in my NEVER USE or SPEND MONEY ON * file. * * The latest version of this file may or may not be * available from the web page at: * * https://www.eq.net/software/chrsh.html * * See the same address for additional documentation, and/or * examples of usage. * */ /* ========== CONFIGURATION ========== */ /* The name of THIS shell */ #define CHRSHNAME "chrsh" /* FULL path to and name of this shell */ #define CHRSHPATH "/bin/chrsh" /* The chroot jail main directory must be owned and permissioned thus */ #define JAILDIRUID 0 #define JAILDIRGID 0 #define JAILDIRMODE 0555 /* The jailed user's chrooted home directory must be this mode */ #define HOMEDIRMODE 0750 /* Log chrsh activity here: */ #define CHRSHLOG "/var/log/chrsh.log" /* All chrsh users MUST have UIDs and GIDs GREATER than these */ #define MINUID 100 #define MINGID 100 /* The final jailed shell must be owned by this UID/GID */ #define SHELLUID 0 #define SHELLGID 0 /* The jailed shell's mode must match this */ #define SHELLMODE 0555 /* REMEMBER to make sure it has execute permission :) */ /* ========== END OF CONFIG ========== */ #include #include #include #include #include #include #include #include #include #include #include #include #include static FILE *logfp = NULL; static char *prog; static uid_t uid; static gid_t gid; static char *username = NULL; static char *groupname = NULL; /* * Open log file for appending, or fail; */ static void open_log(void) { if ((logfp = fopen(CHRSHLOG, "a")) == NULL) { fprintf(stderr, "%s: Failed to open log file!\n", prog); fprintf(stderr, "%s: Call to fopen() failed with error (%d): \"%s\"\n", prog, errno, strerror(errno)); exit(1); } } static char *month[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; static char *weekday[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static void v_write_log(const char *format, va_list ap) { pid_t pid; time_t seconds; struct tm *tm; if (!logfp) { fprintf(stderr, "%s: WARNING: Log file is not open.\n", prog); open_log(); } pid = getpid(); seconds = time(NULL); tm = localtime(&seconds); fprintf(logfp, "%s (%ld) %s %.2d-%s-%d %.2d:%.2d:%.2d ", prog, pid, weekday[tm->tm_wday], tm->tm_mday, month[tm->tm_mon], tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec); if (username != NULL) fprintf(logfp, "USER '%s' (%ld) ", username, uid); else fprintf(logfp, "UID %ld ", uid); if (groupname != NULL) fprintf(logfp, "GROUP '%s' (%ld): ", groupname, gid); else fprintf(logfp, "GID %ld: ", gid); vfprintf(logfp, format, ap); fflush(logfp); } static void write_log(const char *format, ...) { va_list ap; va_start(ap, format); v_write_log(format, ap); va_end(ap); } /* * WARNING: * The routine used below does NOT check individual path components, so * the jailed environment is not guaranteed to be safe. The admin. who * sets up the environment should MAKE SURE that ALL path components of * all jailed users' home subdirectories are correctly permissioned and * owned, as are the chroot main dir, the /bin and /etc subdirs, the * dir in which shells reside, and all other files. I didn't want to * to build the end-all of path/file/directory checking monsters to * check all these factors. That's the admin's job. If any components * are world writable, race conditions and worse WILL almost CERTAINLY * end up in a root compromise. */ int checkjailed(char *item, uid_t uid, gid_t gid, int isdir, mode_t mode) { struct stat statinfo; char *type; type = isdir ? "subdirectory" : "file"; if (lstat(item, &statinfo)) { write_log("Unable to stat %s %s of the jail.\n", item, type); return 1; } if ((!(statinfo.st_mode & S_IFDIR) && isdir) || (!(statinfo.st_mode & S_IFREG) && !isdir)) { write_log("Jailed %s %s is not a %s.\n", item, type, (isdir ? "directory" : "regular file")); return 2; } if (statinfo.st_mode & (S_IWGRP | S_IWOTH)) { write_log("Jailed %s %s is world writable.\n", item, type); return 3; } if ((statinfo.st_mode & ALLPERMS) != mode) { write_log("Jailed %s %s mode does not match expected mode %lo.\n", item, type, mode); return 4; } if (statinfo.st_uid != uid) { write_log("Jailed %s %s is not owned by expected UID %ld.\n", item, type, uid); return 5; } if (statinfo.st_gid != gid) { write_log("Jailed %s %s is not owned by expected GID %ld.\n", item, type, gid); return 6; } return 0; } /* * Case sensitive comparison of an environment variable's * name name with an arbitrary string. Returns 1 if they * match, 0 if they don't. This is backwards from the * basic strcmp() function. */ int envmatch(char *env, char *str) { if (!env || !str) return 0; while (*env && *str) { if (*env++ != *str++) return 0; } return (!*str && *env == '=') ? 1 : 0; } int main(int argc, char *argv[], char *env[]) { char wd[PATH_MAX]; struct passwd *pw; struct group *gr; struct stat statinfo; struct sockaddr_in peer; int found, m, i; char peername[32]; char *gecos; char *home; char *jail; char *shell; char *cmd; char **newargv; char **newenv; char *c; prog = argv[0]; /* * Close EVERY file descriptor EXCEPT stdin, stdout, and stderr: */ for(m = getdtablesize(), i = 3; i < m; i++) { /* * Loop to repeat close() if the call failes with EINTR */ while (close(i) != 0 && errno == EINTR); } /* * Save the calling UID and GID -- these SHOULD be the * UID and GID of the chroot jailed user. */ uid = getuid(); gid = getgid(); /* * Open the log file for appending. */ open_log(); /* * Is STDIN a TCP/IP socket? * If so, get the peer IP address and port number. */ i = sizeof(struct sockaddr_in); if (getpeername(fileno(stdin), (struct sockaddr *)&peer, &i) == 0) { if (peer.sin_family != AF_INET) { write_log("The STDIN socket is of an unrecognized family (%d).\n", peer.sin_family); exit(99); } /* * NOTE: The following sprintf() writes to a FIXED buffer, * a dangerous practice. However, this is the ONLY time the * buffer is written to and there is no way call below can * overrun the buffer (size of 32 chars). If there were ANY * user-supplied data that was not checked for length, I would * use snprintf() or some other method to prevent overruns. */ sprintf(peername, "[%d.%d.%d.%d] port %d", peer.sin_addr.s_addr & 0xff, (peer.sin_addr.s_addr >> 8) & 0xff, (peer.sin_addr.s_addr >> 16) & 0xff, (peer.sin_addr.s_addr >> 24) & 0xff, peer.sin_port); /* NOTE: The above assumes Intel x86 byte ordering */ } else if (errno != ENOTSOCK) { /* Hmm, something WEIRD is going on! */ write_log("Call to getpeername() returned error %d: %s\n", errno, strerror(errno)); exit(100); } else { peername[0] = '\0'; } /* * Are we root? Can do the chroot()? */ if (setgid(0) || setuid(0) || getuid() != geteuid() || getgid() != getegid()) { write_log("Unable to obtain root permission in order to perform chroot() function.\n"); exit(101); } if (uid == 0 || uid < MINUID) { write_log("Cannot operate as forbidden UID %ld.\n", uid); exit(102); } if (gid == 0 || gid < MINGID) { write_log("Cannot operate as forbidden GID %ld.\n", gid); exit(103); } /* * Make sure this program is called with the * correct name and number of arguments. * Remember, a login shell will have a '-' * prepended to it, while an interactive * shell will not. */ if (strcmp(prog, CHRSHNAME) && (*prog != '-' || strcmp(prog+1, CHRSHNAME))) { write_log("invalid program name (%s)\n", prog); exit(111); } /* * Get the current pw structure and save off much of * the data. If the UID can't be found, log the error. */ if ((pw = getpwuid(uid)) == NULL) { write_log("UID %ld does not seem to exist.\n", uid); exit(131); } if (!(username = strdup(pw->pw_name))) { write_log("Unable to allocate memory for user name.\n"); exit(133); } if (gid != pw->pw_gid) { write_log("Running GID %ld does not match password entry GID.\n", gid); exit(134); } if (!(gecos = strdup(pw->pw_gecos))) { write_log("Unable to allocate memory for GECOS information.\n"); exit(135); } if (strcmp(CHRSHPATH,pw->pw_shell)) { write_log("User's shell does not match the full path and name of this program \"%s\".\n", CHRSHPATH); exit(136); } if (!(home = jail = strdup(pw->pw_dir))) { write_log("Unable to allocate memory for home directory.\n"); exit(137); } /* Check the format of the home dir for the /chroot/jail/./home/subdir format: Simple state engine: m == 0 Reading chroot jail part of dir m == 1 Reading chroot jail part of dir, "/" encountered m == 2 Reading separator "/./" -- "." encountered and expecting final "/" m == 3 Reading home subdir part of dir m == 4 Reading home subdir part of dir, "/" encountered */ for (c = home, m = 0; *c; c++) { switch (*c) { case '/': switch (m) { case 0: m = 1; break; case 1: /* ERROR: A double "//" is unacceptable! */ write_log("Unusual double slash \"//\" in chroot jail directory portion of user's home directory.\n"); exit(140); case 2: home = c; m = 4; break; case 3: m = 4; break; default: /* ERROR: A double "//" is unacceptable! */ write_log("Unusual double slash \"//\" in home subdirectory part of user's home directory.\n"); exit(141); } break; case '.': switch (m) { case 0: /* * A '.' character IS permitted in the path * IF it does NOT immediately follow a '/' * slash character. */ m = 0; break; case 1: *(c-1) = '\0'; /* Terminate the chroot jail portion. */ m = 2; break; default: /* ERROR: A period "." should not appear anywhere else. */ write_log("Home subdirectory portion of user's home directory contains a spurious period \".\" character.\n"); } break; default: switch (m) { case 0: break; case 1: m = 0; break; case 2: /* ERROR: Expecting a "/" to begin the subdir part */ write_log("Expecting a slash \"/\" following the jail/subdirectory separator.\n"); exit(143); default: m = 3; } } } if (m == 4) { /* Trim trailing "/" from subdirectory portion */ *(c-1) = '\0'; } #ifdef DEBUG write_log("DEBUG: home==\"%s\" (%ld) jail==\"%s\" (%ld)\n", home, strlen(home), jail, strlen(jail)); #endif if (strlen(jail) < 2) { write_log("Jail portion of home directory must contain a real subdirectory. Using the root directory \"/\" is not permitted.\n"); exit(145); } /* * Now do the same for the group structure, logging the * error if the GID doesn't appear to exist. */ if ((gr = getgrgid(gid)) == NULL) { write_log("GID %ld does not seem to exist.\n", gid); exit(151); } if (!(groupname = strdup(gr->gr_name))) { write_log("Unable to allocate space for user's group name a \"%s\".\n", gr->gr_name); exit(152); } /* * Now check the jail directory out to see if it is properly * owned and permissioned. Change working dir. to the jail * directory and destination for the future chroot(). */ if ((chdir(jail) != 0) || getcwd(wd, PATH_MAX) == NULL || strcmp(jail, wd)) { write_log("Unable to change working directory to the jail directory \"%s\".\n", jail); exit(181); } if (lstat(jail, &statinfo)) { write_log("Unable to stat the jail directory \"%s\".\n", jail); exit(185); } if (statinfo.st_mode & (S_IWGRP | S_IWOTH)) { write_log("Jail directory is world writable.\n"); exit(186); } if ((statinfo.st_mode & ALLPERMS) != JAILDIRMODE) { write_log("Jail directory mode does not match expected mode %lo.\n", JAILDIRMODE); exit(187); } if (statinfo.st_uid != JAILDIRUID) { write_log("Jail directory is not owned by expected UID %ld.\n", JAILDIRUID); exit(188); } if (statinfo.st_gid != JAILDIRGID) { write_log("Jail directory is not owned by expected GID %ld.\n", JAILDIRGID); exit(189); } /* * Now is the TIME! Gonna do it! Here we go... * We are now chroot()-ing to the jail directory... */ if (chroot(jail)) { write_log("Unable to chroot() to the jail directory \"%s\"\n", jail); exit(190); } /* * Now look for and check out /bin and /etc in the jail... */ if (m = checkjailed("/etc", JAILDIRUID, JAILDIRGID, 1, JAILDIRMODE)) { exit(m+200); } if (m = checkjailed("/bin", JAILDIRUID, JAILDIRGID, 1, JAILDIRMODE)) { exit(m+210); } /* * Check out the user's home subdir in the jail... */ if (m = checkjailed(home, uid, gid, 1, HOMEDIRMODE)) { exit(m+220); } /* * Now double-check that the user exists within the jail * environment and that the info. matches what we expect. */ if ((pw = getpwuid(uid)) == NULL) { write_log("UID %ld does not exist in jail environment.\n", uid); exit(231); } if (strcmp(username, pw->pw_name)) { write_log("User's jailed user name does not match \"%s\".\n", username); exit(233); } if (pw->pw_gid != gid) { write_log("User's jailed GID does not match.\n", gid); exit(235); } if (strcmp(gecos, pw->pw_gecos)) { write_log("User's jailed user GECOS information does not match \"%s\".\n", gecos); exit(237); } if (strcmp(home, pw->pw_dir)) { write_log("User's jailed home directory does not match \"%s\".\n", home); exit(238); } /* * Does the user's jailed shell exist? */ for (m=0, shell=getusershell(); !m && shell && *shell; shell=getusershell()) { if (!strcmp(pw->pw_shell, shell)) { m = 1; break; } } endusershell(); if (!m) { write_log("Unable to find user's jailed shell \"%s\" in the jailed version of /etc/shells.\n", pw->pw_shell); exit(240); } if (!(shell = strdup(pw->pw_shell))) { write_log("Unable to allocate space for user's jailed shell \"%s\".\n", pw->pw_shell); exit(241); } if (m = checkjailed(shell, SHELLUID, SHELLGID, 0, SHELLMODE)) { exit(242+m); } /* * Check out the user's jailed group. */ if ((gr = getgrgid(gid)) == NULL) { write_log("Jailed GID %ld does not seem to exist.\n", gid); exit(250); } if (strcmp(groupname, gr->gr_name)) { write_log("Jailed group name does not match \"%s\"\n", groupname); exit(251); } /* * Now become the user. */ if (setgid(gid)) { write_log("Failed to set the GID to user's GID %ld.\n", gid); exit(260); } if (initgroups(username, gid)) { write_log("Failed to initgroups for user \"%s\" GID %ld in jailed environment.\n", username, gid); exit(263); } if (setuid(uid)) { write_log("Failed to set the UID to the user's UID %ld.\n", uid); exit(265); } /* * See if we (as the user) can enter the user's home directory */ if ((m = chdir(home)) != 0 || getcwd(wd, PATH_MAX) == NULL || strcmp(home, wd)) { getcwd(wd, PATH_MAX); write_log("Unable to change working directory to the user's jailed home directory \"%s\".\n", home); exit(269); } /* * See we can execute the shell as the user. If not, log * an error now while we still have access to the log file. */ if (lstat(shell, &statinfo)) { /* * Theoretically, this shouldn't happen since we * already stat'd this file once. Also, this check * is really redundant IF the SHELLMODE define is * correctly defined. */ write_log("Unable to stat user's jailed shell \"%s\"\n", shell); exit(270); } if (!(statinfo.st_mode & S_IXUSR)) { write_log("User cannot execute jailed shell \"%s\" because execute permission is missing.\n", shell); exit(271); } /* * Set up the new environment array, copying everything across * EXCEPT the HOME and SHELL items, which will be recreated. */ /* * First, count how many env. vars there are * EXCEPT the above noted two... */ for (m = 0, newenv = env; *newenv; newenv++, m++) { if (envmatch(*newenv, "HOME") || envmatch(*newenv, "SHELL")) m--; /* Don't count these two env. vars */ } if (!(newenv = (char**)malloc(sizeof(char*) * (m + 3)))) { write_log("Unable to allocate space for the new environment.\n"); exit(280); } /* * Just use the old ones EXCEPT for SHELL and HOME */ for (m = 0, i = 0; env[m] != NULL; m++) { if (!envmatch(env[m], "HOME") && !envmatch(env[m], "SHELL")) { newenv[i++] = env[m]; } } /* * Make a new SHELL env. variable */ if (!(newenv[i] = (char*)malloc(sizeof(char) * (strlen(shell) + 7)))) { write_log("Unable to allocate space for the SHELL environment variable.\n"); exit(281); } strcpy(newenv[i],"SHELL="); strcpy(newenv[i++]+6,shell); /* * Make a new HOME env. variable */ if (!(newenv[i] = (char*)malloc(sizeof(char) * (strlen(home) + 6)))) { write_log("Unable to allocate space for the HOME environment variable.\n"); exit(282); } strcpy(newenv[i],"HOME="); strcpy(newenv[i++]+5,home); /* * Now terminate the env. array */ newenv[i] = NULL; /* * Set up the new argument array. */ for(c = cmd = shell; *c; c++) { if (*c == '/') cmd = c+1; } if (!(newargv = (char**)malloc(sizeof(char*) * argc))) { write_log("unable to allocate space for argv[]\n"); exit(280); } if (*prog == '-') { /* If this program was called with a '-' char. prepended, do the same */ if (!(newargv[0] = (char*)malloc(sizeof(char) * (strlen(cmd) + 2)))) { write_log("Unable to allocate space for argv[0] \"-%s\"\n", cmd); exit(285); } *newargv[0] = '-'; strcpy(newargv[0]+1,cmd); } else { if (!(newargv[0] = strdup(cmd))) { write_log("Unable to allocate space for argv[0] \"%s\"\n", cmd); exit(286); } } /* Pass on any arguments to the real shell */ i = 1; while (i < argc) { if (!(newargv[i] = strdup(argv[i]))) { write_log("Unable to allocate space for user supplied argv[%d]\n", i); exit(287); } i++; } newargv[i] = NULL; /* * Free up some of the space we allocated: */ free(username); free(gecos); free(jail); /* This free's up home too */ free(groupname); /* DON'T free shell 'cause we use it below */ /* Make a hopefully final log entry */ if (*peername) { write_log("User '%s' from %s is jailed. Executing shell '%s'.\n", username, peername, shell); } else { write_log("User '%s' is jailed. Executing shell '%s'.\n", username, shell); } /* * The following call under FreeBSD flags the log file * descriptor so that it will be automatically closed if * the execve() call succeeds, but it will not be closed * if the call fails. This lets us write to the log * file should the call fail, but also protects us * from the subprocess being able to write to the file. * I have no idea how portable this behavior is to * other OSs. */ if (fcntl(fileno(logfp), F_SETFD, 1) == -1) { write_log("Unable to flag log file for automatic closing. Call to fcntl() failed: (%d) \"%s\"\n", errno, strerror(errno)); exit(290); } /* * THE TIME OF FINAL JUDGEMENT HAS ARRIVED! * Execute the command, replacing our image with its own. */ execve(shell, newargv, newenv); /* * The earth blew up and everybody died! * Log a whole lot of stuff to help track down the problem. */ write_log("Bad kharma! The chrsh execve() call failed!\n"); write_log(" shell==\"%s\" cmd==\"%s\"\n", shell, cmd); for (i=0; newargv[i]; i++) write_log(" arg[%d]==\"%s\"\n", i, newargv[i]); for (i=0; newenv[i]; i++) write_log(" env[%d]==\"%s\"\n", i, newenv[i]); write_log("Check the shell executable to make sure it is valid and not corrupt.\n"); fclose(logfp); /* Theoretically we never get to here, right? ;) */ exit(1000); }