/* * Copyright (c) 1999-2005, 2007-2018 Todd C. Miller * * 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 #ifdef HAVE_PAM #include #include #include #ifdef HAVE_STRING_H # include #endif /* HAVE_STRING_H */ #ifdef HAVE_STRINGS_H # include #endif /* HAVE_STRINGS_H */ #include #include #include #ifdef HAVE_PAM_PAM_APPL_H # include #else # include #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 */