/****************************************************************************** * A module for Linux-PAM that will cache authentication results, inspired by * (and implemented with an eye toward being mixable with) sudo. * * Copyright (c) 2002 Red Hat, Inc. * Written by Nalin Dahyabhai * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, and the entire permission notice in its entirety, * including the disclaimer of warranties. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * ALTERNATIVELY, this product may be distributed under the terms of * the GNU Public License, in which case the provisions of the GPL are * required INSTEAD OF the above restrictions. (This clause is * necessary due to a potential bad interaction between the GPL and * the restrictions contained in a BSD-style copyright.) * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS 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 THE AUTHOR 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. * */ #define PAM_SM_AUTH #define PAM_SM_SESSION #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "hmacsha1.h" #include #include #include #include /* The default timeout we use is 5 minutes, which matches the sudo default * for the timestamp_timeout parameter. */ #define DEFAULT_TIMESTAMP_TIMEOUT (5 * 60) #define MODULE "pam_timestamp" #define TIMESTAMPDIR _PATH_VARRUN "/" MODULE #define TIMESTAMPKEY TIMESTAMPDIR "/_pam_timestamp_key" /* Various buffers we use need to be at least as large as either PATH_MAX or * LINE_MAX, so choose the larger of the two. */ #if (LINE_MAX > PATH_MAX) #define BUFLEN LINE_MAX #else #define BUFLEN PATH_MAX #endif /* Return PAM_SUCCESS if the given directory looks "safe". */ static int check_dir_perms(pam_handle_t *pamh, const char *tdir) { char scratch[BUFLEN]; struct stat st; int i; /* Check that the directory is "safe". */ if ((tdir == NULL) || (strlen(tdir) == 0)) { return PAM_AUTH_ERR; } /* Iterate over the path, checking intermediate directories. */ memset(scratch, 0, sizeof(scratch)); for (i = 0; (tdir[i] != '\0') && (i < (int)sizeof(scratch)); i++) { scratch[i] = tdir[i]; if ((scratch[i] == '/') || (tdir[i + 1] == '\0')) { /* We now have the name of a directory in the path, so * we need to check it. */ if ((lstat(scratch, &st) == -1) && (errno != ENOENT)) { pam_syslog(pamh, LOG_ERR, "unable to read `%s': %m", scratch); return PAM_AUTH_ERR; } if (!S_ISDIR(st.st_mode)) { pam_syslog(pamh, LOG_ERR, "`%s' is not a directory", scratch); return PAM_AUTH_ERR; } if (S_ISLNK(st.st_mode)) { pam_syslog(pamh, LOG_ERR, "`%s' is a symbolic link", scratch); return PAM_AUTH_ERR; } if (st.st_uid != 0) { pam_syslog(pamh, LOG_ERR, "`%s' owner UID != 0", scratch); return PAM_AUTH_ERR; } if (st.st_gid != 0) { pam_syslog(pamh, LOG_ERR, "`%s' owner GID != 0", scratch); return PAM_AUTH_ERR; } if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0) { pam_syslog(pamh, LOG_ERR, "`%s' permissions are lax", scratch); return PAM_AUTH_ERR; } } } return PAM_SUCCESS; } /* Validate a tty pathname as actually belonging to a tty, and return its base * name if it's valid. */ static const char * check_tty(const char *tty) { /* Check that we're not being set up to take a fall. */ if ((tty == NULL) || (strlen(tty) == 0)) { return NULL; } /* Pull out the meaningful part of the tty's name. */ if (strchr(tty, '/') != NULL) { if (strncmp(tty, "/dev/", 5) != 0) { /* Make sure the device node is actually in /dev/, * noted by Michal Zalewski. */ return NULL; } tty = strrchr(tty, '/') + 1; } /* Make sure the tty wasn't actually a directory (no basename). */ if (!strlen(tty) || !strcmp(tty, ".") || !strcmp(tty, "..")) { return NULL; } return tty; } /* Determine the right path name for a given user's timestamp. */ static int format_timestamp_name(char *path, size_t len, const char *timestamp_dir, const char *tty, const char *ruser, const char *user) { if (strcmp(ruser, user) == 0) { return snprintf(path, len, "%s/%s/%s", timestamp_dir, ruser, tty); } else { return snprintf(path, len, "%s/%s/%s:%s", timestamp_dir, ruser, tty, user); } } /* Check if a given timestamp date, when compared to a current time, fits * within the given interval. */ static int timestamp_good(time_t then, time_t now, time_t interval) { if (((now >= then) && ((now - then) < interval)) || ((now < then) && ((then - now) < (2 * interval)))) { return PAM_SUCCESS; } return PAM_AUTH_ERR; } static int check_login_time(const char *ruser, time_t timestamp) { struct utmp utbuf, *ut; time_t oldest_login = 0; setutent(); while( #ifdef HAVE_GETUTENT_R !getutent_r(&utbuf, &ut) #else (ut = getutent()) != NULL #endif ) { if (ut->ut_type != USER_PROCESS) { continue; } if (strncmp(ruser, ut->ut_user, sizeof(ut->ut_user)) != 0) { continue; } if (oldest_login == 0 || oldest_login > ut->ut_tv.tv_sec) { oldest_login = ut->ut_tv.tv_sec; } } endutent(); if(oldest_login == 0 || timestamp < oldest_login) { return PAM_AUTH_ERR; } return PAM_SUCCESS; } #ifndef PAM_TIMESTAMP_MAIN static int get_ruser(pam_handle_t *pamh, char *ruserbuf, size_t ruserbuflen) { const void *ruser; struct passwd *pwd; if (ruserbuf == NULL || ruserbuflen < 1) return -2; /* Get the name of the source user. */ if (pam_get_item(pamh, PAM_RUSER, &ruser) != PAM_SUCCESS) { ruser = NULL; } if ((ruser == NULL) || (strlen(ruser) == 0)) { /* Barring that, use the current RUID. */ pwd = pam_modutil_getpwuid(pamh, getuid()); if (pwd != NULL) { ruser = pwd->pw_name; } } else { /* * This ruser is used by format_timestamp_name as a component * of constructed timestamp pathname, so ".", "..", and '/' * are disallowed to avoid potential path traversal issues. */ if (!strcmp(ruser, ".") || !strcmp(ruser, "..") || strchr(ruser, '/')) { ruser = NULL; } } if (ruser == NULL || strlen(ruser) >= ruserbuflen) { *ruserbuf = '\0'; return -1; } strcpy(ruserbuf, ruser); return 0; } /* Get the path to the timestamp to use. */ static int get_timestamp_name(pam_handle_t *pamh, int argc, const char **argv, char *path, size_t len) { const char *user, *tty; const void *void_tty; const char *tdir = TIMESTAMPDIR; char ruser[BUFLEN]; int i, debug = 0; /* Parse arguments. */ for (i = 0; i < argc; i++) { if (strcmp(argv[i], "debug") == 0) { debug = 1; } } for (i = 0; i < argc; i++) { if (strncmp(argv[i], "timestampdir=", 13) == 0) { tdir = argv[i] + 13; if (debug) { pam_syslog(pamh, LOG_DEBUG, "storing timestamps in `%s'", tdir); } } } i = check_dir_perms(pamh, tdir); if (i != PAM_SUCCESS) { return i; } /* Get the name of the target user. */ if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS) { user = NULL; } if ((user == NULL) || (strlen(user) == 0)) { return PAM_AUTH_ERR; } if (debug) { pam_syslog(pamh, LOG_DEBUG, "becoming user `%s'", user); } /* Get the name of the source user. */ if (get_ruser(pamh, ruser, sizeof(ruser)) || strlen(ruser) == 0) { return PAM_AUTH_ERR; } if (debug) { pam_syslog(pamh, LOG_DEBUG, "currently user `%s'", ruser); } /* Get the name of the terminal. */ if (pam_get_item(pamh, PAM_TTY, &void_tty) != PAM_SUCCESS) { tty = NULL; } else { tty = void_tty; } if ((tty == NULL) || (strlen(tty) == 0)) { tty = ttyname(STDIN_FILENO); if ((tty == NULL) || (strlen(tty) == 0)) { tty = ttyname(STDOUT_FILENO); } if ((tty == NULL) || (strlen(tty) == 0)) { tty = ttyname(STDERR_FILENO); } if ((tty == NULL) || (strlen(tty) == 0)) { /* Match sudo's behavior for this case. */ tty = "unknown"; } } if (debug) { pam_syslog(pamh, LOG_DEBUG, "tty is `%s'", tty); } /* Snip off all but the last part of the tty name. */ tty = check_tty(tty); if (tty == NULL) { return PAM_AUTH_ERR; } /* Generate the name of the file used to cache auth results. These * paths should jive with sudo's per-tty naming scheme. */ if (format_timestamp_name(path, len, tdir, tty, ruser, user) >= (int)len) { return PAM_AUTH_ERR; } if (debug) { pam_syslog(pamh, LOG_DEBUG, "using timestamp file `%s'", path); } return PAM_SUCCESS; } /* Tell the user that access has been granted. */ static void verbose_success(pam_handle_t *pamh, long diff) { pam_info(pamh, _("Access has been granted" " (last access was %ld seconds ago)."), diff); } int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct stat st; time_t interval = DEFAULT_TIMESTAMP_TIMEOUT; int i, fd, debug = 0, verbose = 0; char path[BUFLEN], *p, *message, *message_end; long tmp; const void *void_service; const char *service; time_t now, then; /* Parse arguments. */ for (i = 0; i < argc; i++) { if (strcmp(argv[i], "debug") == 0) { debug = 1; } } for (i = 0; i < argc; i++) { if (strncmp(argv[i], "timestamp_timeout=", 18) == 0) { tmp = strtol(argv[i] + 18, &p, 0); if ((p != NULL) && (*p == '\0')) { interval = tmp; if (debug) { pam_syslog(pamh, LOG_DEBUG, "setting timeout to %ld" " seconds", (long)interval); } } } else if (strcmp(argv[i], "verbose") == 0) { verbose = 1; if (debug) { pam_syslog(pamh, LOG_DEBUG, "becoming more verbose"); } } } if (flags & PAM_SILENT) { verbose = 0; } /* Get the name of the timestamp file. */ if (get_timestamp_name(pamh, argc, argv, path, sizeof(path)) != PAM_SUCCESS) { return PAM_AUTH_ERR; } /* Get the name of the service. */ if (pam_get_item(pamh, PAM_SERVICE, &void_service) != PAM_SUCCESS) { service = NULL; } else { service = void_service; } if ((service == NULL) || (strlen(service) == 0)) { service = "(unknown)"; } /* Open the timestamp file. */ fd = open(path, O_RDONLY | O_NOFOLLOW); if (fd == -1) { if (debug) { pam_syslog(pamh, LOG_DEBUG, "cannot open timestamp `%s': %m", path); } return PAM_AUTH_ERR; } if (fstat(fd, &st) == 0) { int count; void *mac; size_t maclen; char ruser[BUFLEN]; /* Check that the file is owned by the superuser. */ if ((st.st_uid != 0) || (st.st_gid != 0)) { pam_syslog(pamh, LOG_ERR, "timestamp file `%s' is " "not owned by root", path); close(fd); return PAM_AUTH_ERR; } /* Check that the file is a normal file. */ if (!(S_ISREG(st.st_mode))) { pam_syslog(pamh, LOG_ERR, "timestamp file `%s' is " "not a regular file", path); close(fd); return PAM_AUTH_ERR; } /* Check that the file is the expected size. */ if (st.st_size == 0) { /* Invalid, but may have been created by sudo. */ close(fd); return PAM_AUTH_ERR; } if (st.st_size != (off_t)(strlen(path) + 1 + sizeof(then) + hmac_sha1_size())) { pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' " "appears to be corrupted", path); close(fd); return PAM_AUTH_ERR; } /* Read the file contents. */ message = malloc(st.st_size); count = 0; if (!message) { close(fd); return PAM_BUF_ERR; } while (count < st.st_size) { i = read(fd, message + count, st.st_size - count); if ((i == 0) || (i == -1)) { break; } count += i; } if (count < st.st_size) { pam_syslog(pamh, LOG_NOTICE, "error reading timestamp " "file `%s': %m", path); close(fd); free(message); return PAM_AUTH_ERR; } message_end = message + strlen(path) + 1 + sizeof(then); /* Regenerate the MAC. */ hmac_sha1_generate_file(pamh, &mac, &maclen, TIMESTAMPKEY, 0, 0, message, message_end - message); if ((mac == NULL) || (memcmp(path, message, strlen(path)) != 0) || (memcmp(mac, message_end, maclen) != 0)) { pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is " "corrupted", path); close(fd); free(mac); free(message); return PAM_AUTH_ERR; } free(mac); memmove(&then, message + strlen(path) + 1, sizeof(then)); free(message); /* Check oldest login against timestamp */ if (get_ruser(pamh, ruser, sizeof(ruser))) { close(fd); return PAM_AUTH_ERR; } if (check_login_time(ruser, then) != PAM_SUCCESS) { pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is " "older than oldest login, disallowing " "access to %s for user %s", path, service, ruser); close(fd); return PAM_AUTH_ERR; } /* Compare the dates. */ now = time(NULL); if (timestamp_good(then, now, interval) == PAM_SUCCESS) { close(fd); pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is " "only %ld seconds old, allowing access to %s " "for user %s", path, (long) (now - st.st_mtime), service, ruser); if (verbose) { verbose_success(pamh, now - st.st_mtime); } return PAM_SUCCESS; } else { close(fd); pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' has " "unacceptable age (%ld seconds), disallowing " "access to %s for user %s", path, (long) (now - st.st_mtime), service, ruser); return PAM_AUTH_ERR; } } close(fd); /* Fail by default. */ return PAM_AUTH_ERR; } int pam_sm_setcred(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED, const char **argv UNUSED) { return PAM_SUCCESS; } int pam_sm_open_session(pam_handle_t *pamh, int flags UNUSED, int argc, const char **argv) { char path[BUFLEN], subdir[BUFLEN], *text, *p; void *mac; size_t maclen; time_t now; int fd, i, debug = 0; /* Parse arguments. */ for (i = 0; i < argc; i++) { if (strcmp(argv[i], "debug") == 0) { debug = 1; } } /* Get the name of the timestamp file. */ if (get_timestamp_name(pamh, argc, argv, path, sizeof(path)) != PAM_SUCCESS) { return PAM_SESSION_ERR; } /* Create the directory for the timestamp file if it doesn't already * exist. */ for (i = 1; i < (int) sizeof(path) && path[i] != '\0'; i++) { if (path[i] == '/') { /* Attempt to create the directory. */ memcpy(subdir, path, i); subdir[i] = '\0'; if (mkdir(subdir, 0700) == 0) { /* Attempt to set the owner to the superuser. */ if (lchown(subdir, 0, 0) != 0) { if (debug) { pam_syslog(pamh, LOG_DEBUG, "error setting permissions on `%s': %m", subdir); } return PAM_SESSION_ERR; } } else { if (errno != EEXIST) { if (debug) { pam_syslog(pamh, LOG_DEBUG, "error creating directory `%s': %m", subdir); } return PAM_SESSION_ERR; } } } } /* Generate the message. */ text = malloc(strlen(path) + 1 + sizeof(now) + hmac_sha1_size()); if (text == NULL) { pam_syslog(pamh, LOG_CRIT, "unable to allocate memory: %m"); return PAM_SESSION_ERR; } p = text; strcpy(text, path); p += strlen(path) + 1; now = time(NULL); memmove(p, &now, sizeof(now)); p += sizeof(now); /* Generate the MAC and append it to the plaintext. */ hmac_sha1_generate_file(pamh, &mac, &maclen, TIMESTAMPKEY, 0, 0, text, p - text); if (mac == NULL) { pam_syslog(pamh, LOG_ERR, "failure generating MAC: %m"); free(text); return PAM_SESSION_ERR; } memmove(p, mac, maclen); p += maclen; free(mac); /* Open the file. */ fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); if (fd == -1) { pam_syslog(pamh, LOG_ERR, "unable to open `%s': %m", path); free(text); return PAM_SESSION_ERR; } /* Attempt to set the owner to the superuser. */ if (fchown(fd, 0, 0) != 0) { if (debug) { pam_syslog(pamh, LOG_DEBUG, "error setting ownership of `%s': %m", path); } close(fd); free(text); return PAM_SESSION_ERR; } /* Write the timestamp to the file. */ if (write(fd, text, p - text) != p - text) { pam_syslog(pamh, LOG_ERR, "unable to write to `%s': %m", path); close(fd); free(text); return PAM_SESSION_ERR; } /* Close the file and return successfully. */ close(fd); free(text); pam_syslog(pamh, LOG_DEBUG, "updated timestamp file `%s'", path); return PAM_SUCCESS; } int pam_sm_close_session(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED, const char **argv UNUSED) { return PAM_SUCCESS; } #else /* PAM_TIMESTAMP_MAIN */ #define USAGE "Usage: %s [[-k] | [-d]] [target user]\n" #define CHECK_INTERVAL 7 int main(int argc, char **argv) { int i, retval = 0, dflag = 0, kflag = 0; const char *target_user = NULL, *user = NULL, *tty = NULL; struct passwd *pwd; struct timeval tv; fd_set write_fds; char path[BUFLEN]; struct stat st; /* Check that there's nothing funny going on with stdio. */ if ((fstat(STDIN_FILENO, &st) == -1) || (fstat(STDOUT_FILENO, &st) == -1) || (fstat(STDERR_FILENO, &st) == -1)) { /* Appropriate the "no controlling tty" error code. */ return 3; } /* Parse arguments. */ while ((i = getopt(argc, argv, "dk")) != -1) { switch (i) { case 'd': dflag++; break; case 'k': kflag++; break; default: fprintf(stderr, USAGE, argv[0]); return 1; break; } } /* Bail if both -k and -d are given together. */ if ((kflag + dflag) > 1) { fprintf(stderr, USAGE, argv[0]); return 1; } /* Check that we're setuid. */ if (geteuid() != 0) { fprintf(stderr, "%s must be setuid root\n", argv[0]); retval = 2; } /* Check that we have a controlling tty. */ tty = ttyname(STDIN_FILENO); if ((tty == NULL) || (strlen(tty) == 0)) { tty = ttyname(STDOUT_FILENO); } if ((tty == NULL) || (strlen(tty) == 0)) { tty = ttyname(STDERR_FILENO); } if ((tty == NULL) || (strlen(tty) == 0)) { tty = "unknown"; } /* Get the name of the invoking (requesting) user. */ pwd = getpwuid(getuid()); if (pwd == NULL) { retval = 4; } /* Get the name of the target user. */ user = strdup(pwd->pw_name); if (user == NULL) { retval = 4; } else { target_user = (optind < argc) ? argv[optind] : user; if ((strchr(target_user, '.') != NULL) || (strchr(target_user, '/') != NULL) || (strchr(target_user, '%') != NULL)) { fprintf(stderr, "unknown user: %s\n", target_user); retval = 4; } } /* Sanity check the tty to make sure we should be checking * for timestamps which pertain to it. */ if (retval == 0) { tty = check_tty(tty); if (tty == NULL) { fprintf(stderr, "invalid tty\n"); retval = 6; } } do { /* Sanity check the timestamp directory itself. */ if (retval == 0) { if (check_dir_perms(NULL, TIMESTAMPDIR) != PAM_SUCCESS) { retval = 5; } } if (retval == 0) { /* Generate the name of the timestamp file. */ format_timestamp_name(path, sizeof(path), TIMESTAMPDIR, tty, user, target_user); } if (retval == 0) { if (kflag) { /* Remove the timestamp. */ if (lstat(path, &st) != -1) { retval = unlink(path); } } else { /* Check the timestamp. */ if (lstat(path, &st) != -1) { /* Check oldest login against timestamp */ if (check_login_time(user, st.st_mtime) != PAM_SUCCESS) { retval = 7; } else if (timestamp_good(st.st_mtime, time(NULL), DEFAULT_TIMESTAMP_TIMEOUT) != PAM_SUCCESS) { retval = 7; } } else { retval = 7; } } } if (dflag > 0) { struct timeval now; /* Send the would-be-returned value to our parent. */ signal(SIGPIPE, SIG_DFL); fprintf(stdout, "%d\n", retval); fflush(stdout); /* Wait. */ gettimeofday(&now, NULL); tv.tv_sec = CHECK_INTERVAL; /* round the sleep time to get woken up on a whole second */ tv.tv_usec = 1000000 - now.tv_usec; if (now.tv_usec < 500000) tv.tv_sec--; FD_ZERO(&write_fds); FD_SET(STDOUT_FILENO, &write_fds); select(STDOUT_FILENO + 1, NULL, NULL, &write_fds, &tv); retval = 0; } } while (dflag > 0); return retval; } #endif