diff options
Diffstat (limited to 'plugins/sudoers/auth/pam.c')
-rw-r--r-- | plugins/sudoers/auth/pam.c | 616 |
1 files changed, 616 insertions, 0 deletions
diff --git a/plugins/sudoers/auth/pam.c b/plugins/sudoers/auth/pam.c new file mode 100644 index 0000000..129e4fe --- /dev/null +++ b/plugins/sudoers/auth/pam.c @@ -0,0 +1,616 @@ +/* + * Copyright (c) 1999-2005, 2007-2018 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#ifdef HAVE_PAM + +#include <sys/types.h> +#include <stdio.h> +#include <stdlib.h> +#ifdef HAVE_STRING_H +# include <string.h> +#endif /* HAVE_STRING_H */ +#ifdef HAVE_STRINGS_H +# include <strings.h> +#endif /* HAVE_STRINGS_H */ +#include <unistd.h> +#include <pwd.h> +#include <errno.h> + +#ifdef HAVE_PAM_PAM_APPL_H +# include <pam/pam_appl.h> +#else +# include <security/pam_appl.h> +#endif + +#ifdef HAVE_LIBINTL_H +# if defined(__LINUX_PAM__) +# define PAM_TEXT_DOMAIN "Linux-PAM" +# elif defined(__sun__) +# define PAM_TEXT_DOMAIN "SUNW_OST_SYSOSPAM" +# endif +#endif + +/* We don't want to translate the strings in the calls to dgt(). */ +#ifdef PAM_TEXT_DOMAIN +# define dgt(d, t) dgettext(d, t) +#endif + +#include "sudoers.h" +#include "sudo_auth.h" + +/* Only OpenPAM and Linux PAM use const qualifiers. */ +#ifdef PAM_SUN_CODEBASE +# define PAM_CONST +#else +# define PAM_CONST const +#endif + +/* Ambiguity in spec: is it an array of pointers or a pointer to an array? */ +#ifdef PAM_SUN_CODEBASE +# define PAM_MSG_GET(msg, n) (*(msg) + (n)) +#else +# define PAM_MSG_GET(msg, n) ((msg)[(n)]) +#endif + +#ifndef PAM_DATA_SILENT +#define PAM_DATA_SILENT 0 +#endif + +static int converse(int, PAM_CONST struct pam_message **, + struct pam_response **, void *); +static struct sudo_conv_callback *conv_callback; +static struct pam_conv pam_conv = { converse, &conv_callback }; +static char *def_prompt = PASSPROMPT; +static bool getpass_error; +static pam_handle_t *pamh; + +static int +sudo_pam_init2(struct passwd *pw, sudo_auth *auth, bool quiet) +{ + static int pam_status = PAM_SUCCESS; + int rc; + debug_decl(sudo_pam_init, SUDOERS_DEBUG_AUTH) + + /* Stash pointer to last pam status. */ + auth->data = &pam_status; + +#ifdef _AIX + if (pamh != NULL) { + /* Already initialized (may happen with AIX). */ + debug_return_int(AUTH_SUCCESS); + } +#endif /* _AIX */ + + /* Initial PAM setup */ + pam_status = pam_start(ISSET(sudo_mode, MODE_LOGIN_SHELL) ? + def_pam_login_service : def_pam_service, pw->pw_name, &pam_conv, &pamh); + if (pam_status != PAM_SUCCESS) { + if (!quiet) + log_warning(0, N_("unable to initialize PAM")); + debug_return_int(AUTH_FATAL); + } + + /* + * Set PAM_RUSER to the invoking user (the "from" user). + * We set PAM_RHOST to avoid a bug in Solaris 7 and below. + */ + rc = pam_set_item(pamh, PAM_RUSER, user_name); + if (rc != PAM_SUCCESS) { + const char *errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_set_item(pamh, PAM_RUSER, %s): %s", user_name, + errstr ? errstr : "unknown error"); + } +#ifdef __sun__ + rc = pam_set_item(pamh, PAM_RHOST, user_host); + if (rc != PAM_SUCCESS) { + const char *errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_set_item(pamh, PAM_RHOST, %s): %s", user_host, + errstr ? errstr : "unknown error"); + } +#endif + + /* + * Some versions of pam_lastlog have a bug that + * will cause a crash if PAM_TTY is not set so if + * there is no tty, set PAM_TTY to the empty string. + */ + rc = pam_set_item(pamh, PAM_TTY, user_ttypath ? user_ttypath : ""); + if (rc != PAM_SUCCESS) { + const char *errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_set_item(pamh, PAM_TTY, %s): %s", + user_ttypath ? user_ttypath : "", errstr ? errstr : "unknown error"); + } + + /* + * If PAM session and setcred support is disabled we don't + * need to keep a sudo process around to close the session. + */ + if (!def_pam_session && !def_pam_setcred) + auth->end_session = NULL; + + debug_return_int(AUTH_SUCCESS); +} + +int +sudo_pam_init(struct passwd *pw, sudo_auth *auth) +{ + return sudo_pam_init2(pw, auth, false); +} + +#ifdef _AIX +int +sudo_pam_init_quiet(struct passwd *pw, sudo_auth *auth) +{ + return sudo_pam_init2(pw, auth, true); +} +#endif /* _AIX */ + +int +sudo_pam_verify(struct passwd *pw, char *prompt, sudo_auth *auth, struct sudo_conv_callback *callback) +{ + const char *s; + int *pam_status = (int *) auth->data; + debug_decl(sudo_pam_verify, SUDOERS_DEBUG_AUTH) + + def_prompt = prompt; /* for converse */ + getpass_error = false; /* set by converse if user presses ^C */ + conv_callback = callback; /* passed to conversation function */ + + /* PAM_SILENT prevents the authentication service from generating output. */ + *pam_status = pam_authenticate(pamh, PAM_SILENT); + if (getpass_error) { + /* error or ^C from tgetpass() */ + debug_return_int(AUTH_INTR); + } + switch (*pam_status) { + case PAM_SUCCESS: + debug_return_int(AUTH_SUCCESS); + case PAM_AUTH_ERR: + case PAM_AUTHINFO_UNAVAIL: + case PAM_MAXTRIES: + case PAM_PERM_DENIED: + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "pam_authenticate: %d", *pam_status); + debug_return_int(AUTH_FAILURE); + default: + if ((s = pam_strerror(pamh, *pam_status)) != NULL) + log_warningx(0, N_("PAM authentication error: %s"), s); + debug_return_int(AUTH_FATAL); + } +} + +int +sudo_pam_approval(struct passwd *pw, sudo_auth *auth, bool exempt) +{ + const char *s; + int rc, status = AUTH_SUCCESS; + int *pam_status = (int *) auth->data; + debug_decl(sudo_pam_approval, SUDOERS_DEBUG_AUTH) + + rc = pam_acct_mgmt(pamh, PAM_SILENT); + switch (rc) { + case PAM_SUCCESS: + break; + case PAM_AUTH_ERR: + log_warningx(0, N_("account validation failure, " + "is your account locked?")); + status = AUTH_FATAL; + break; + case PAM_NEW_AUTHTOK_REQD: + /* Ignore if user is exempt from password restrictions. */ + if (exempt) { + rc = *pam_status; + break; + } + /* New password required, try to change it. */ + log_warningx(0, N_("Account or password is " + "expired, reset your password and try again")); + rc = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK); + if (rc == PAM_SUCCESS) + break; + if ((s = pam_strerror(pamh, rc)) == NULL) + s = "unknown error"; + log_warningx(0, + N_("unable to change expired password: %s"), s); + status = AUTH_FAILURE; + break; + case PAM_AUTHTOK_EXPIRED: + /* Ignore if user is exempt from password restrictions. */ + if (exempt) { + rc = *pam_status; + break; + } + /* Password expired, cannot be updated by user. */ + log_warningx(0, + N_("Password expired, contact your system administrator")); + status = AUTH_FATAL; + break; + case PAM_ACCT_EXPIRED: + log_warningx(0, + N_("Account expired or PAM config lacks an \"account\" " + "section for sudo, contact your system administrator")); + status = AUTH_FATAL; + break; + case PAM_AUTHINFO_UNAVAIL: + case PAM_MAXTRIES: + case PAM_PERM_DENIED: + s = pam_strerror(pamh, rc); + log_warningx(0, N_("PAM account management error: %s"), + s ? s : "unknown error"); + status = AUTH_FAILURE; + break; + default: + s = pam_strerror(pamh, rc); + log_warningx(0, N_("PAM account management error: %s"), + s ? s : "unknown error"); + status = AUTH_FATAL; + break; + } + *pam_status = rc; + debug_return_int(status); +} + +int +sudo_pam_cleanup(struct passwd *pw, sudo_auth *auth) +{ + int *pam_status = (int *) auth->data; + debug_decl(sudo_pam_cleanup, SUDOERS_DEBUG_AUTH) + + /* If successful, we can't close the session until sudo_pam_end_session() */ + if (*pam_status != PAM_SUCCESS || auth->end_session == NULL) { + *pam_status = pam_end(pamh, *pam_status | PAM_DATA_SILENT); + pamh = NULL; + } + debug_return_int(*pam_status == PAM_SUCCESS ? AUTH_SUCCESS : AUTH_FAILURE); +} + +int +sudo_pam_begin_session(struct passwd *pw, char **user_envp[], sudo_auth *auth) +{ + int rc, status = AUTH_SUCCESS; + int *pam_status = (int *) auth->data; + const char *errstr; + debug_decl(sudo_pam_begin_session, SUDOERS_DEBUG_AUTH) + + /* + * If there is no valid user we cannot open a PAM session. + * This is not an error as sudo can run commands with arbitrary + * uids, it just means we are done from a session management standpoint. + */ + if (pw == NULL) { + if (pamh != NULL) { + rc = pam_end(pamh, PAM_SUCCESS | PAM_DATA_SILENT); + if (rc != PAM_SUCCESS) { + errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_end: %s", errstr ? errstr : "unknown error"); + } + pamh = NULL; + } + goto done; + } + + /* + * Update PAM_USER to reference the user we are running the command + * as, as opposed to the user we authenticated as. + */ + rc = pam_set_item(pamh, PAM_USER, pw->pw_name); + if (rc != PAM_SUCCESS) { + errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_set_item(pamh, PAM_USER, %s): %s", pw->pw_name, + errstr ? errstr : "unknown error"); + } + + /* + * Reinitialize credentials when changing the user. + * We don't worry about a failure from pam_setcred() since with + * stacked PAM auth modules a failure from one module may override + * PAM_SUCCESS from another. For example, given a non-local user, + * pam_unix will fail but pam_ldap or pam_sss may succeed, but if + * pam_unix is first in the stack, pam_setcred() will fail. + */ + if (def_pam_setcred) { + rc = pam_setcred(pamh, PAM_REINITIALIZE_CRED); + if (rc != PAM_SUCCESS) { + errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_setcred: %s", errstr ? errstr : "unknown error"); + } + } + + if (def_pam_session) { + /* + * We use PAM_SILENT to prevent pam_lastlog from printing last login + * information except when explicitly running a shell. + */ + const bool silent = !ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL); + rc = pam_open_session(pamh, silent ? PAM_SILENT : 0); + switch (rc) { + case PAM_SUCCESS: + break; + case PAM_SESSION_ERR: + /* Treat PAM_SESSION_ERR as a non-fatal error. */ + errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_open_session: %s", errstr ? errstr : "unknown error"); + /* Avoid closing session that was not opened. */ + def_pam_session = false; + break; + default: + /* Unexpected session failure, treat as fatal error. */ + *pam_status = rc; + errstr = pam_strerror(pamh, *pam_status); + log_warningx(0, N_("%s: %s"), "pam_open_session", + errstr ? errstr : "unknown error"); + rc = pam_end(pamh, *pam_status | PAM_DATA_SILENT); + if (rc != PAM_SUCCESS) { + errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_end: %s", errstr ? errstr : "unknown error"); + } + pamh = NULL; + status = AUTH_FATAL; + goto done; + } + } + +#ifdef HAVE_PAM_GETENVLIST + /* + * Update environment based on what is stored in pamh. + * If no authentication is done we will only have environment + * variables if pam_env is called via session. + */ + if (user_envp != NULL) { + char **pam_envp = pam_getenvlist(pamh); + if (pam_envp != NULL) { + /* Merge pam env with user env. */ + if (!env_init(*user_envp) || !env_merge(pam_envp)) + status = AUTH_FATAL; + *user_envp = env_get(); + (void)env_init(NULL); + free(pam_envp); + /* XXX - we leak any duplicates that were in pam_envp */ + } + } +#endif /* HAVE_PAM_GETENVLIST */ + +done: + debug_return_int(status); +} + +int +sudo_pam_end_session(struct passwd *pw, sudo_auth *auth) +{ + int rc, status = AUTH_SUCCESS; + debug_decl(sudo_pam_end_session, SUDOERS_DEBUG_AUTH) + + if (pamh != NULL) { + /* + * Update PAM_USER to reference the user we are running the command + * as, as opposed to the user we authenticated as. + * XXX - still needed now that session init is in parent? + */ + rc = pam_set_item(pamh, PAM_USER, pw->pw_name); + if (rc != PAM_SUCCESS) { + const char *errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_set_item(pamh, PAM_USER, %s): %s", pw->pw_name, + errstr ? errstr : "unknown error"); + } + if (def_pam_session) { + rc = pam_close_session(pamh, PAM_SILENT); + if (rc != PAM_SUCCESS) { + const char *errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_close_session: %s", errstr ? errstr : "unknown error"); + } + } + if (def_pam_setcred) { + rc = pam_setcred(pamh, PAM_DELETE_CRED | PAM_SILENT); + if (rc != PAM_SUCCESS) { + const char *errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_setcred: %s", errstr ? errstr : "unknown error"); + } + } + rc = pam_end(pamh, PAM_SUCCESS | PAM_DATA_SILENT); + if (rc != PAM_SUCCESS) { + const char *errstr = pam_strerror(pamh, rc); + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "pam_end: %s", errstr ? errstr : "unknown error"); + status = AUTH_FATAL; + } + pamh = NULL; + } + + debug_return_int(status); +} + +#define PROMPT_IS_PASSWORD(_p) \ + (strncmp((_p), "Password:", 9) == 0 && \ + ((_p)[9] == '\0' || ((_p)[9] == ' ' && (_p)[10] == '\0'))) + +#ifdef PAM_TEXT_DOMAIN +# define PAM_PROMPT_IS_PASSWORD(_p) \ + (strcmp((_p), dgt(PAM_TEXT_DOMAIN, "Password:")) == 0 || \ + strcmp((_p), dgt(PAM_TEXT_DOMAIN, "Password: ")) == 0 || \ + PROMPT_IS_PASSWORD(_p)) +#else +# define PAM_PROMPT_IS_PASSWORD(_p) PROMPT_IS_PASSWORD(_p) +#endif /* PAM_TEXT_DOMAIN */ + +/* + * We use the PAM prompt in preference to sudo's as long + * as passprompt_override is not set and: + * a) the (translated) sudo prompt matches /^Password: ?/ + * or: + * b) the PAM prompt itself *doesn't* match /^Password: ?/ + * or /^username's Password: ?/ + * + * The intent is to use the PAM prompt for things like + * challenge-response, otherwise use sudo's prompt. + * There may also be cases where a localized translation + * of "Password: " exists for PAM but not for sudo. + */ +static bool +use_pam_prompt(const char *pam_prompt) +{ + size_t user_len; + debug_decl(use_pam_prompt, SUDOERS_DEBUG_AUTH) + + /* Always use sudo prompt if passprompt_override is set. */ + if (def_passprompt_override) + debug_return_bool(false); + + /* If sudo prompt matches "^Password: ?$", use PAM prompt. */ + if (PROMPT_IS_PASSWORD(def_prompt)) + debug_return_bool(true); + + /* If PAM prompt matches "^Password: ?$", use sudo prompt. */ + if (PAM_PROMPT_IS_PASSWORD(pam_prompt)) + debug_return_bool(false); + + /* + * Some PAM modules use "^username's Password: ?$" instead of + * "^Password: ?" so check for that too. + */ + user_len = strlen(user_name); + if (strncmp(pam_prompt, user_name, user_len) == 0) { + const char *cp = pam_prompt + user_len; + if (strncmp(cp, "'s Password:", 12) == 0 && + (cp[12] == '\0' || (cp[12] == ' ' && cp[13] == '\0'))) + debug_return_bool(false); + } + + /* Otherwise, use the PAM prompt. */ + debug_return_bool(true); +} + +/* + * ``Conversation function'' for PAM <-> human interaction. + */ +static int +converse(int num_msg, PAM_CONST struct pam_message **msg, + struct pam_response **reply_out, void *vcallback) +{ + struct sudo_conv_callback *callback = NULL; + struct pam_response *reply; + const char *prompt; + char *pass; + int n, type; + int ret = PAM_SUCCESS; + debug_decl(converse, SUDOERS_DEBUG_AUTH) + + if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "invalid number of PAM messages: %d", num_msg); + debug_return_int(PAM_CONV_ERR); + } + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "number of PAM messages: %d", num_msg); + + if ((reply = calloc(num_msg, sizeof(struct pam_response))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_int(PAM_BUF_ERR); + } + *reply_out = reply; + + if (vcallback != NULL) + callback = *((struct sudo_conv_callback **)vcallback); + + for (n = 0; n < num_msg; n++) { + PAM_CONST struct pam_message *pm = PAM_MSG_GET(msg, n); + + type = SUDO_CONV_PROMPT_ECHO_OFF; + switch (pm->msg_style) { + case PAM_PROMPT_ECHO_ON: + type = SUDO_CONV_PROMPT_ECHO_ON; + /* FALLTHROUGH */ + case PAM_PROMPT_ECHO_OFF: + /* Error out if the last password read was interrupted. */ + if (getpass_error) + goto done; + + /* Choose either the sudo prompt or the PAM one. */ + prompt = use_pam_prompt(pm->msg) ? pm->msg : def_prompt; + + /* Read the password unless interrupted. */ + pass = auth_getpass(prompt, type, callback); + if (pass == NULL) { + /* Error (or ^C) reading password, don't try again. */ + getpass_error = true; + ret = PAM_CONV_ERR; + goto done; + } + if (strlen(pass) >= PAM_MAX_RESP_SIZE) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "password longer than %d", PAM_MAX_RESP_SIZE); + ret = PAM_CONV_ERR; + memset_s(pass, SUDO_CONV_REPL_MAX, 0, strlen(pass)); + goto done; + } + reply[n].resp = pass; /* auth_getpass() malloc's a copy */ + break; + case PAM_TEXT_INFO: + if (pm->msg != NULL) + sudo_printf(SUDO_CONV_INFO_MSG, "%s\n", pm->msg); + break; + case PAM_ERROR_MSG: + if (pm->msg != NULL) + sudo_printf(SUDO_CONV_ERROR_MSG, "%s\n", pm->msg); + break; + default: + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unsupported message style: %d", pm->msg_style); + ret = PAM_CONV_ERR; + goto done; + } + } + +done: + if (ret != PAM_SUCCESS) { + /* Zero and free allocated memory and return an error. */ + for (n = 0; n < num_msg; n++) { + struct pam_response *pr = &reply[n]; + + if (pr->resp != NULL) { + memset_s(pr->resp, SUDO_CONV_REPL_MAX, 0, strlen(pr->resp)); + free(pr->resp); + pr->resp = NULL; + } + } + free(reply); + *reply_out = NULL; + } + debug_return_int(ret); +} + +#endif /* HAVE_PAM */ |