Commit message: pam_timestamp: fix potential directory traversal issue (ticket #27)

pam_timestamp uses values of PAM_RUSER and PAM_TTY as components of
the timestamp pathname it creates, so extra care should be taken to
avoid potential directory traversal issues.

* modules/pam_timestamp/pam_timestamp.c (check_tty): Treat
"." and ".." tty values as invalid.
(get_ruser): Treat "." and ".." ruser values, as well as any ruser
value containing '/', as invalid.

Fixes CVE-2014-2583.

Reported-by: Sebastian Krahmer <krahmer@suse.de>
CVE: CVE-2014-2583
CWE: CWE-22
Vulnerability description: Multiple directory traversal vulnerabilities in pam_timestamp.c in the pam_timestamp module for Linux-PAM (aka pam) 1.1.8 allow local users to create arbitrary files or possibly bypass authentication via a .. (dot dot) in the (1) PAM_RUSER value to the get_ruser function or (2) PAM_TTY value to the check_tty function, which is used by the format_timestamp_name function.

Project: linux-pam
File: pam_timestamp.c

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;
}

Project: linux-pam
File: pam_timestamp.c

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);
	}
}

Project: linux-pam
File: pam_timestamp.c

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;
}

Project: linux-pam
File: pam_timestamp.c

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;
}

Project: linux-pam
File: pam_timestamp.c

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;
}

Project: linux-pam
File: pam_timestamp.c

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; path[i] != '\0'; i++) {
		if (path[i] == '/') {
			/* Attempt to create the directory. */
			strncpy(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_ERR, "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;
}

Project: linux-pam
File: pam_timestamp.c

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;
}

Project: linux-pam
File: pam_timestamp.c

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;
}

Project: linux-pam
File: pam_timestamp.c

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;
}

Project: linux-pam
File: pam_timestamp.c

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;
}

Project: linux-pam
File: pam_timestamp.c

verbose_success(pam_handle_t *pamh, long diff)
{
	pam_info(pamh, _("Access granted (last access was %ld seconds ago)."), diff);
}