From 50b37d4a27d3295a29afca2286f1a5a086142cec Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 11:49:46 +0200 Subject: Adding upstream version 3.2.1+dfsg. Signed-off-by: Daniel Baumann --- src/modules/rlm_mschap/rlm_mschap.c | 2150 +++++++++++++++++++++++++++++++++++ 1 file changed, 2150 insertions(+) create mode 100644 src/modules/rlm_mschap/rlm_mschap.c (limited to 'src/modules/rlm_mschap/rlm_mschap.c') diff --git a/src/modules/rlm_mschap/rlm_mschap.c b/src/modules/rlm_mschap/rlm_mschap.c new file mode 100644 index 0000000..25bcd9c --- /dev/null +++ b/src/modules/rlm_mschap/rlm_mschap.c @@ -0,0 +1,2150 @@ +/* + * This program is is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * $Id$ + * @file rlm_mschap.c + * @brief Implemented mschap authentication. + * + * @copyright 2000,2001,2006 The FreeRADIUS server project + */ + +/* MPPE support from Takahiro Wagatsuma */ +RCSID("$Id$") + +#include +#include +#include +#include +#include + +#include + +#include "rlm_mschap.h" +#include "mschap.h" +#include "smbdes.h" + +#ifdef WITH_AUTH_WINBIND +#include "auth_wbclient.h" +#endif + +#ifdef HAVE_OPENSSL_CRYPTO_H +USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */ +# include +#endif + +#ifdef __APPLE__ +int od_mschap_auth(REQUEST *request, VALUE_PAIR *challenge, VALUE_PAIR * usernamepair); +#endif + +/* Allowable account control bits */ +#define ACB_DISABLED 0x00010000 //!< User account disabled. +#define ACB_HOMDIRREQ 0x00020000 //!< Home directory required. +#define ACB_PWNOTREQ 0x00040000 //!< User password not required. +#define ACB_TEMPDUP 0x00080000 //!< Temporary duplicate account. +#define ACB_NORMAL 0x00100000 //!< Normal user account. +#define ACB_MNS 0x00200000 //!< MNS logon user account. +#define ACB_DOMTRUST 0x00400000 //!< Interdomain trust account. +#define ACB_WSTRUST 0x00800000 //!< Workstation trust account. +#define ACB_SVRTRUST 0x01000000 //!< Server trust account. +#define ACB_PWNOEXP 0x02000000 //!< User password does not expire. +#define ACB_AUTOLOCK 0x04000000 //!< Account auto locked. +#define ACB_PW_EXPIRED 0x00020000 //!< Password Expired. + +static int pdb_decode_acct_ctrl(char const *p) +{ + int acct_ctrl = 0; + int done = 0; + + /* + * Check if the account type bits have been encoded after the + * NT password (in the form [NDHTUWSLXI]). + */ + + if (*p != '[') return 0; + + for (p++; *p && !done; p++) { + switch (*p) { + case 'N': /* 'N'o password. */ + acct_ctrl |= ACB_PWNOTREQ; + break; + + case 'D': /* 'D'isabled. */ + acct_ctrl |= ACB_DISABLED ; + break; + + case 'H': /* 'H'omedir required. */ + acct_ctrl |= ACB_HOMDIRREQ; + break; + + case 'T': /* 'T'emp account. */ + acct_ctrl |= ACB_TEMPDUP; + break; + + case 'U': /* 'U'ser account (normal). */ + acct_ctrl |= ACB_NORMAL; + break; + + case 'M': /* 'M'NS logon user account. What is this? */ + acct_ctrl |= ACB_MNS; + break; + + case 'W': /* 'W'orkstation account. */ + acct_ctrl |= ACB_WSTRUST; + break; + + case 'S': /* 'S'erver account. */ + acct_ctrl |= ACB_SVRTRUST; + break; + + case 'L': /* 'L'ocked account. */ + acct_ctrl |= ACB_AUTOLOCK; + break; + + case 'X': /* No 'X'piry on password */ + acct_ctrl |= ACB_PWNOEXP; + break; + + case 'I': /* 'I'nterdomain trust account. */ + acct_ctrl |= ACB_DOMTRUST; + break; + + case 'e': /* 'e'xpired, the password has */ + acct_ctrl |= ACB_PW_EXPIRED; + break; + + case ' ': /* ignore spaces */ + break; + + case ':': + case '\n': + case '\0': + case ']': + default: + done = 1; + break; + } + } + + return acct_ctrl; +} + + +/* + * Does dynamic translation of strings. + * + * Pulls NT-Response, LM-Response, or Challenge from MSCHAP + * attributes. + */ +static ssize_t mschap_xlat(void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + size_t i, data_len; + uint8_t const *data = NULL; + uint8_t buffer[32]; + VALUE_PAIR *user_name; + VALUE_PAIR *chap_challenge, *response; + rlm_mschap_t *inst = instance; + + response = NULL; + + /* + * Challenge means MS-CHAPv1 challenge, or + * hash of MS-CHAPv2 challenge, and peer challenge. + */ + if (strncasecmp(fmt, "Challenge", 9) == 0) { + chap_challenge = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT, TAG_ANY); + if (!chap_challenge) { + REDEBUG("No MS-CHAP-Challenge in the request"); + return -1; + } + + /* + * MS-CHAP-Challenges are 8 octets, + * for MS-CHAPv1 + */ + if (chap_challenge->vp_length == 8) { + RDEBUG2("mschap1: %02x", chap_challenge->vp_octets[0]); + data = chap_challenge->vp_octets; + data_len = 8; + + /* + * MS-CHAP-Challenges are 16 octets, + * for MS-CHAPv2. + */ + } else if (chap_challenge->vp_length == 16) { + VALUE_PAIR *name_attr, *response_name; + char const *username_string; + + response = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP2_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY); + if (!response) { + REDEBUG("MS-CHAP2-Response is required to calculate MS-CHAPv1 challenge"); + return -1; + } + + /* + * FIXME: Much of this is copied from + * below. We should put it into a + * separate function. + */ + + /* + * Responses are 50 octets. + */ + if (response->vp_length < 50) { + REDEBUG("MS-CHAP-Response has the wrong format"); + return -1; + } + + user_name = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + if (!user_name) { + REDEBUG("User-Name is required to calculate MS-CHAPv1 Challenge"); + return -1; + } + + /* + * Check for MS-CHAP-User-Name and if found, use it + * to construct the MSCHAPv1 challenge. This is + * set by rlm_eap_mschap to the MS-CHAP Response + * packet Name field. + * + * We prefer this to the User-Name in the + * packet. + */ + response_name = fr_pair_find_by_num(request->packet->vps, PW_MS_CHAP_USER_NAME, 0, TAG_ANY); + if (response_name) { + name_attr = response_name; + } else { + name_attr = user_name; + } + + /* + * with_ntdomain_hack moved here, too. + */ + if ((username_string = strchr(name_attr->vp_strvalue, '\\')) != NULL) { + if (inst->with_ntdomain_hack) { + username_string++; + } else { + RWDEBUG2("NT Domain delimiter found, should we have enabled with_ntdomain_hack?"); + username_string = name_attr->vp_strvalue; + } + } else { + username_string = name_attr->vp_strvalue; + } + + if (response_name && + ((user_name->vp_length != response_name->vp_length) || + (strncasecmp(user_name->vp_strvalue, response_name->vp_strvalue, + user_name->vp_length) != 0))) { + RWDEBUG2("User-Name (%s) is not the same as MS-CHAP Name (%s) from EAP-MSCHAPv2", + user_name->vp_strvalue, response_name->vp_strvalue); + } + + /* + * Get the MS-CHAPv1 challenge + * from the MS-CHAPv2 peer challenge, + * our challenge, and the user name. + */ + RDEBUG2("Creating challenge hash with username: %s", username_string); + mschap_challenge_hash(response->vp_octets + 2, + chap_challenge->vp_octets, + username_string, buffer); + data = buffer; + data_len = 8; + } else { + REDEBUG("Invalid MS-CHAP challenge length"); + return -1; + } + + /* + * Get the MS-CHAPv1 response, or the MS-CHAPv2 + * response. + */ + } else if (strncasecmp(fmt, "NT-Response", 11) == 0) { + response = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY); + if (!response) response = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP2_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY); + if (!response) { + REDEBUG("No MS-CHAP-Response or MS-CHAP2-Response was found in the request"); + return -1; + } + + /* + * For MS-CHAPv1, the NT-Response exists only + * if the second octet says so. + */ + if ((response->da->vendor == VENDORPEC_MICROSOFT) && + (response->da->attr == PW_MSCHAP_RESPONSE) && + ((response->vp_octets[1] & 0x01) == 0)) { + REDEBUG("No NT-Response in MS-CHAP-Response"); + return -1; + } + + /* + * MS-CHAP-Response and MS-CHAP2-Response have + * the NT-Response at the same offset, and are + * the same length. + */ + data = response->vp_octets + 26; + data_len = 24; + + /* + * LM-Response is deprecated, and exists only + * in MS-CHAPv1, and not often there. + */ + } else if (strncasecmp(fmt, "LM-Response", 11) == 0) { + response = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY); + if (!response) { + REDEBUG("No MS-CHAP-Response was found in the request"); + return -1; + } + + /* + * For MS-CHAPv1, the LM-Response exists only + * if the second octet says so. + */ + if ((response->vp_octets[1] & 0x01) != 0) { + REDEBUG("No LM-Response in MS-CHAP-Response"); + return -1; + } + data = response->vp_octets + 2; + data_len = 24; + + /* + * Pull the domain name out of the User-Name, if it exists. + * + * This is the full domain name, not just the name after host/ + */ + } else if (strncasecmp(fmt, "Domain-Name", 11) == 0) { + char *p; + + user_name = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + if (!user_name) { + REDEBUG("No User-Name was found in the request"); + return -1; + } + + /* + * First check to see if this is a host/ style User-Name + * (a la Kerberos host principal) + */ + if (strncmp(user_name->vp_strvalue, "host/", 5) == 0) { + /* + * If we're getting a User-Name formatted in this way, + * it's likely due to PEAP. The Windows Domain will be + * the first domain component following the hostname, + * or the machine name itself if only a hostname is supplied + */ + p = strchr(user_name->vp_strvalue, '.'); + if (!p) { + RDEBUG2("setting NT-Domain to same as machine name"); + strlcpy(out, user_name->vp_strvalue + 5, outlen); + } else { + p++; /* skip the period */ + strlcpy(out, p, outlen); + } + } else { + p = strchr(user_name->vp_strvalue, '\\'); + if (!p) { + REDEBUG("No NT-Domain was found in the User-Name"); + return -1; + } + + /* + * Hack. This is simpler than the alternatives. + */ + *p = '\0'; + strlcpy(out, user_name->vp_strvalue, outlen); + *p = '\\'; + } + + return strlen(out); + + /* + * Pull the NT-Domain out of the User-Name, if it exists. + */ + } else if (strncasecmp(fmt, "NT-Domain", 9) == 0) { + char *p, *q; + + user_name = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + if (!user_name) { + REDEBUG("No User-Name was found in the request"); + return -1; + } + + /* + * First check to see if this is a host/ style User-Name + * (a la Kerberos host principal) + */ + if (strncmp(user_name->vp_strvalue, "host/", 5) == 0) { + /* + * If we're getting a User-Name formatted in this way, + * it's likely due to PEAP. The Windows Domain will be + * the first domain component following the hostname, + * or the machine name itself if only a hostname is supplied + */ + p = strchr(user_name->vp_strvalue, '.'); + if (!p) { + RDEBUG2("setting NT-Domain to same as machine name"); + strlcpy(out, user_name->vp_strvalue + 5, outlen); + } else { + p++; /* skip the period */ + q = strchr(p, '.'); + /* + * use the same hack as below + * only if another period was found + */ + if (q) *q = '\0'; + strlcpy(out, p, outlen); + if (q) *q = '.'; + } + } else { + p = strchr(user_name->vp_strvalue, '\\'); + if (!p) { + REDEBUG("No NT-Domain was found in the User-Name"); + return -1; + } + + /* + * Hack. This is simpler than the alternatives. + */ + *p = '\0'; + strlcpy(out, user_name->vp_strvalue, outlen); + *p = '\\'; + } + + return strlen(out); + + /* + * Pull the User-Name out of the User-Name... + */ + } else if (strncasecmp(fmt, "User-Name", 9) == 0) { + char const *p, *q; + + user_name = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + if (!user_name) { + REDEBUG("No User-Name was found in the request"); + return -1; + } + + /* + * First check to see if this is a host/ style User-Name + * (a la Kerberos host principal) + */ + if (strncmp(user_name->vp_strvalue, "host/", 5) == 0) { + p = user_name->vp_strvalue + 5; + /* + * If we're getting a User-Name formatted in this way, + * it's likely due to PEAP. When authenticating this against + * a Domain, Windows will expect the User-Name to be in the + * format of hostname$, the SAM version of the name, so we + * have to convert it to that here. We do so by stripping + * off the first 5 characters (host/), and copying everything + * from that point to the first period into a string and appending + * a $ to the end. + */ + q = strchr(p, '.'); + + /* + * use the same hack as above + * only if a period was found + */ + if (q) { + snprintf(out, outlen, "%.*s$", + (int) (q - p), p); + } else { + snprintf(out, outlen, "%s$", p); + } + } else { + p = strchr(user_name->vp_strvalue, '\\'); + if (p) { + p++; /* skip the backslash */ + } else { + p = user_name->vp_strvalue; /* use the whole User-Name */ + } + strlcpy(out, p, outlen); + } + + return strlen(out); + + /* + * Return the NT-Hash of the passed string + */ + } else if (strncasecmp(fmt, "NT-Hash ", 8) == 0) { + char const *p; + + p = fmt + 8; /* 7 is the length of 'NT-Hash' */ + if ((*p == '\0') || (outlen <= 32)) + return 0; + + while (isspace(*p)) p++; + + if (mschap_ntpwdhash(buffer, p) < 0) { + REDEBUG("Failed generating NT-Password"); + *buffer = '\0'; + return -1; + } + + fr_bin2hex(out, buffer, NT_DIGEST_LENGTH); + out[32] = '\0'; + RDEBUG("NT-Hash of \"known-good\" password: %s", out); + return 32; + + /* + * Return the LM-Hash of the passed string + */ + } else if (strncasecmp(fmt, "LM-Hash ", 8) == 0) { + char const *p; + + p = fmt + 8; /* 7 is the length of 'LM-Hash' */ + if ((*p == '\0') || (outlen <= 32)) + return 0; + + while (isspace(*p)) p++; + + smbdes_lmpwdhash(p, buffer); + fr_bin2hex(out, buffer, LM_DIGEST_LENGTH); + out[32] = '\0'; + RDEBUG("LM-Hash of %s = %s", p, out); + return 32; + } else { + REDEBUG("Unknown expansion string '%s'", fmt); + return -1; + } + + if (outlen == 0) return 0; /* nowhere to go, don't do anything */ + + /* + * Didn't set anything: this is bad. + */ + if (!data) { + RWDEBUG2("Failed to do anything intelligent"); + return 0; + } + + /* + * Check the output length. + */ + if (outlen < ((data_len * 2) + 1)) { + data_len = (outlen - 1) / 2; + } + + /* + * + */ + for (i = 0; i < data_len; i++) { + sprintf(out + (2 * i), "%02x", data[i]); + } + out[data_len * 2] = '\0'; + + return data_len * 2; +} + + +#ifdef WITH_AUTH_WINBIND +/* + * Free connection pool winbind context + */ +static int _mod_conn_free(struct wbcContext **wb_ctx) +{ + wbcCtxFree(*wb_ctx); + + return 0; +} + +/* + * Create connection pool winbind context + */ +static void *mod_conn_create(TALLOC_CTX *ctx, UNUSED void *instance) +{ + struct wbcContext **wb_ctx; + + wb_ctx = talloc_zero(ctx, struct wbcContext *); + *wb_ctx = wbcCtxCreate(); + + if (*wb_ctx == NULL) { + ERROR("failed to create winbind context"); + talloc_free(wb_ctx); + return NULL; + } + + talloc_set_destructor(wb_ctx, _mod_conn_free); + + return *wb_ctx; +} +#endif + + +static const CONF_PARSER passchange_config[] = { + { "ntlm_auth", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_mschap_t, ntlm_cpw), NULL }, + { "ntlm_auth_username", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_mschap_t, ntlm_cpw_username), NULL }, + { "ntlm_auth_domain", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_mschap_t, ntlm_cpw_domain), NULL }, + { "local_cpw", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_mschap_t, local_cpw), NULL }, + CONF_PARSER_TERMINATOR +}; + +static const CONF_PARSER module_config[] = { + /* + * Cache the password by default. + */ + { "use_mppe", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_mschap_t, use_mppe), "yes" }, + { "require_encryption", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_mschap_t, require_encryption), "no" }, + { "require_strong", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_mschap_t, require_strong), "no" }, + { "with_ntdomain_hack", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_mschap_t, with_ntdomain_hack), "yes" }, + { "ntlm_auth", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_mschap_t, ntlm_auth), NULL }, + { "ntlm_auth_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_mschap_t, ntlm_auth_timeout), NULL }, + { "passchange", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) passchange_config }, + { "allow_retry", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_mschap_t, allow_retry), "yes" }, + { "retry_msg", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_mschap_t, retry_msg), NULL }, + { "winbind_username", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, rlm_mschap_t, wb_username), NULL }, + { "winbind_domain", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, rlm_mschap_t, wb_domain), NULL }, + { "winbind_retry_with_normalised_username", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_mschap_t, wb_retry_with_normalised_username), "no" }, +#ifdef __APPLE__ + { "use_open_directory", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_mschap_t, open_directory), "yes" }, +#endif + CONF_PARSER_TERMINATOR +}; + + +static int mod_bootstrap(CONF_SECTION *conf, void *instance) +{ + char const *name; + rlm_mschap_t *inst = instance; + + /* + * Create the dynamic translation. + */ + name = cf_section_name2(conf); + if (!name) name = cf_section_name1(conf); + inst->xlat_name = name; + xlat_register(inst->xlat_name, mschap_xlat, NULL, inst); + + return 0; +} + +/* + * Create instance for our module. Allocate space for + * instance structure and read configuration parameters + */ +static int mod_instantiate(CONF_SECTION *conf, void *instance) +{ + rlm_mschap_t *inst = instance; + + /* + * For backwards compatibility + */ + if (!dict_valbyname(PW_AUTH_TYPE, 0, inst->xlat_name)) { + inst->auth_type = "MS-CHAP"; + } else { + inst->auth_type = inst->xlat_name; + } + + /* + * Set auth method + */ + inst->method = AUTH_INTERNAL; + + if (inst->wb_username) { +#ifdef WITH_AUTH_WINBIND + inst->method = AUTH_WBCLIENT; + + inst->wb_pool = fr_connection_pool_module_init(conf, inst, mod_conn_create, NULL, NULL); + if (!inst->wb_pool) { + cf_log_err_cs(conf, "Unable to initialise winbind connection pool"); + return -1; + } +#else + cf_log_err_cs(conf, "'winbind' is not enabled in this build."); + return -1; +#endif + } + + /* preserve existing behaviour: this option overrides all */ + if (inst->ntlm_auth) { + inst->method = AUTH_NTLMAUTH_EXEC; + } + + switch (inst->method) { + case AUTH_INTERNAL: + DEBUG("rlm_mschap (%s): using internal authentication", inst->xlat_name); + break; + case AUTH_NTLMAUTH_EXEC: + DEBUG("rlm_mschap (%s): authenticating by calling 'ntlm_auth'", inst->xlat_name); + break; +#ifdef WITH_AUTH_WINBIND + case AUTH_WBCLIENT: + DEBUG("rlm_mschap (%s): authenticating directly to winbind", inst->xlat_name); + break; +#endif + } + + /* + * Check ntlm_auth_timeout is sane + */ + if (!inst->ntlm_auth_timeout) { + inst->ntlm_auth_timeout = EXEC_TIMEOUT; + } + if (inst->ntlm_auth_timeout < 1) { + cf_log_err_cs(conf, "ntml_auth_timeout '%d' is too small (minimum: 1)", + inst->ntlm_auth_timeout); + return -1; + } + if (inst->ntlm_auth_timeout > 10) { + cf_log_err_cs(conf, "ntlm_auth_timeout '%d' is too large (maximum: 10)", + inst->ntlm_auth_timeout); + return -1; + } + + return 0; +} + +/* + * Tidy up instance + */ +static int mod_detach(UNUSED void *instance) +{ +#ifdef WITH_AUTH_WINBIND + rlm_mschap_t *inst = instance; + + fr_connection_pool_free(inst->wb_pool); +#endif + + return 0; +} + +/* + * add_reply() adds either MS-CHAP2-Success or MS-CHAP-Error + * attribute to reply packet + */ +void mschap_add_reply(REQUEST *request, unsigned char ident, + char const *name, char const *value, size_t len) +{ + VALUE_PAIR *vp; + + vp = pair_make_reply(name, NULL, T_OP_EQ); + if (!vp) { + REDEBUG("Failed to create attribute %s: %s", name, fr_strerror()); + return; + } + + /* Account for the ident byte */ + vp->vp_length = len + 1; + if (vp->da->type == PW_TYPE_STRING) { + char *p; + + vp->vp_strvalue = p = talloc_array(vp, char, vp->vp_length + 1); + p[vp->vp_length] = '\0'; /* Always \0 terminate */ + p[0] = ident; + memcpy(p + 1, value, len); + } else { + uint8_t *p; + + vp->vp_octets = p = talloc_array(vp, uint8_t, vp->vp_length); + p[0] = ident; + memcpy(p + 1, value, len); + } +} + +/* + * Add MPPE attributes to the reply. + */ +static void mppe_add_reply(REQUEST *request, char const* name, uint8_t const * value, size_t len) +{ + VALUE_PAIR *vp; + + vp = pair_make_reply(name, NULL, T_OP_EQ); + if (!vp) { + REDEBUG("mppe_add_reply failed to create attribute %s: %s", name, fr_strerror()); + return; + } + + fr_pair_value_memcpy(vp, value, len); +} + +static int write_all(int fd, char const *buf, int len) { + int rv,done=0; + + while (done < len) { + rv = write(fd, buf+done, len-done); + if (rv <= 0) + break; + done += rv; + } + return done; +} + +/* + * Perform an MS-CHAP2 password change + */ + +static int CC_HINT(nonnull (1, 2, 4, 5)) do_mschap_cpw(rlm_mschap_t *inst, + REQUEST *request, +#ifdef HAVE_OPENSSL_CRYPTO_H + VALUE_PAIR *nt_password, +#else + UNUSED VALUE_PAIR *nt_password, +#endif + uint8_t *new_nt_password, + uint8_t *old_nt_hash, + MSCHAP_AUTH_METHOD method) +{ + if (inst->ntlm_cpw && method != AUTH_INTERNAL) { + /* + * we're going to run ntlm_auth in helper-mode + * we're expecting to use the ntlm-change-password-1 protocol + * which needs the following on stdin: + * + * username: %{mschap:User-Name} + * nt-domain: %{mschap:NT-Domain} + * new-nt-password-blob: bin2hex(new_nt_password) - 1032 bytes encoded + * old-nt-hash-blob: bin2hex(old_nt_hash) - 32 bytes encoded + * new-lm-password-blob: 00000...0000 - 1032 bytes null + * old-lm-hash-blob: 000....000 - 32 bytes null + * .\n + * + * ...and it should then print out + * + * Password-Change: Yes + * + * or + * + * Password-Change: No + * Password-Change-Error: blah + */ + + int to_child=-1; + int from_child=-1; + pid_t pid, child_pid; + int status, len; + char buf[2048]; + char *pmsg; + char const *emsg; + + RDEBUG("Doing MS-CHAPv2 password change via ntlm_auth helper"); + + /* + * Start up ntlm_auth with a pipe on stdin and stdout + */ + + pid = radius_start_program(inst->ntlm_cpw, request, true, &to_child, &from_child, NULL, false); + if (pid < 0) { + REDEBUG("could not exec ntlm_auth cpw command"); + return -1; + } + + /* + * write the stuff to the client + */ + + if (inst->ntlm_cpw_username) { + len = radius_xlat(buf, sizeof(buf) - 2, request, inst->ntlm_cpw_username, NULL, NULL); + if (len < 0) { + goto ntlm_auth_err; + } + + buf[len++] = '\n'; + buf[len] = '\0'; + + if (write_all(to_child, buf, len) != len) { + REDEBUG("Failed to write username to child"); + goto ntlm_auth_err; + } + } else { + RWDEBUG2("No ntlm_auth username set, passchange will definitely fail!"); + } + + if (inst->ntlm_cpw_domain) { + len = radius_xlat(buf, sizeof(buf) - 2, request, inst->ntlm_cpw_domain, NULL, NULL); + if (len < 0) { + goto ntlm_auth_err; + } + + buf[len++] = '\n'; + buf[len] = '\0'; + + if (write_all(to_child, buf, len) != len) { + REDEBUG("Failed to write domain to child"); + goto ntlm_auth_err; + } + } else { + RWDEBUG2("No ntlm_auth domain set, username must be full-username to work"); + } + + /* now the password blobs */ + len = sprintf(buf, "new-nt-password-blob: "); + fr_bin2hex(buf+len, new_nt_password, 516); + buf[len+1032] = '\n'; + buf[len+1033] = '\0'; + len = strlen(buf); + if (write_all(to_child, buf, len) != len) { + RDEBUG2("failed to write new password blob to child"); + goto ntlm_auth_err; + } + + len = sprintf(buf, "old-nt-hash-blob: "); + fr_bin2hex(buf+len, old_nt_hash, NT_DIGEST_LENGTH); + buf[len+32] = '\n'; + buf[len+33] = '\0'; + len = strlen(buf); + if (write_all(to_child, buf, len) != len) { + REDEBUG("Failed to write old hash blob to child"); + goto ntlm_auth_err; + } + + /* + * In current samba versions, failure to supply empty LM password/hash + * blobs causes the change to fail. + */ + len = sprintf(buf, "new-lm-password-blob: %01032i\n", 0); + if (write_all(to_child, buf, len) != len) { + REDEBUG("Failed to write dummy LM password to child"); + goto ntlm_auth_err; + } + len = sprintf(buf, "old-lm-hash-blob: %032i\n", 0); + if (write_all(to_child, buf, len) != len) { + REDEBUG("Failed to write dummy LM hash to child"); + goto ntlm_auth_err; + } + if (write_all(to_child, ".\n", 2) != 2) { + REDEBUG("Failed to send finish to child"); + goto ntlm_auth_err; + } + close(to_child); + to_child = -1; + + /* + * Read from the child + */ + len = radius_readfrom_program(from_child, pid, 10, buf, sizeof(buf)); + if (len < 0) { + /* radius_readfrom_program will have closed from_child for us */ + REDEBUG("Failure reading from child"); + return -1; + } + close(from_child); + from_child = -1; + + buf[len] = 0; + RDEBUG2("ntlm_auth said: %s", buf); + + child_pid = rad_waitpid(pid, &status); + if (child_pid == 0) { + REDEBUG("Timeout waiting for child"); + return -1; + } + if (child_pid != pid) { + REDEBUG("Abnormal exit status: %s", fr_syserror(errno)); + return -1; + } + + if (strstr(buf, "Password-Change: Yes")) { + RDEBUG2("ntlm_auth password change succeeded"); + return 0; + } + + pmsg = strstr(buf, "Password-Change-Error: "); + if (pmsg) { + emsg = strsep(&pmsg, "\n"); + } else { + emsg = "could not find error"; + } + REDEBUG("ntlm auth password change failed: %s", emsg); + +ntlm_auth_err: + /* safe because these either need closing or are == -1 */ + close(to_child); + close(from_child); + + return -1; + + } else if (inst->local_cpw) { +#ifdef HAVE_OPENSSL_CRYPTO_H + /* + * Decrypt the new password blob, add it as a temporary request + * variable, xlat the local_cpw string, then remove it + * + * this allows is to write e..g + * + * %{sql:insert into ...} + * + * ...or... + * + * %{exec:/path/to %{mschap:User-Name} %{MS-CHAP-New-Password}}" + * + */ + VALUE_PAIR *new_pass, *new_hash; + uint8_t *p, *q; + char *x; + size_t i; + size_t passlen; + ssize_t result_len; + char result[253]; + uint8_t nt_pass_decrypted[516], old_nt_hash_expected[NT_DIGEST_LENGTH]; + + if (!nt_password) { + RDEBUG("Local MS-CHAPv2 password change requires NT-Password attribute"); + return -1; + } else { + RDEBUG("Doing MS-CHAPv2 password change locally"); + } + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + { + EVP_CIPHER_CTX *ctx; + int ntlen = sizeof(nt_pass_decrypted); + + ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + REDEBUG("Failed getting RC4 from OpenSSL"); + error: + if (ctx) EVP_CIPHER_CTX_free(ctx); + return -1; + } + + if (!EVP_CIPHER_CTX_set_key_length(ctx, nt_password->vp_length)) { + REDEBUG("Failed setting key length"); + goto error; + } + + if (!EVP_EncryptInit_ex(ctx, EVP_rc4(), NULL, nt_password->vp_octets, NULL)) { + REDEBUG("Failed setting key value"); + goto error;; + } + + if (!EVP_EncryptUpdate(ctx, nt_pass_decrypted, &ntlen, new_nt_password, ntlen)) { + REDEBUG("Failed getting output"); + goto error; + } + + EVP_CIPHER_CTX_free(ctx); + } +#else + { + RC4_KEY key; + + /* + * Decrypt the blob + */ + RC4_set_key(&key, nt_password->vp_length, nt_password->vp_octets); + RC4(&key, 516, new_nt_password, nt_pass_decrypted); + } +#endif + + /* + * pwblock is + * 512-N bytes random pad + * N bytes password as utf-16-le + * 4 bytes - N as big-endian int + */ + passlen = nt_pass_decrypted[512]; + passlen += nt_pass_decrypted[513] << 8; + if ((nt_pass_decrypted[514] != 0) || + (nt_pass_decrypted[515] != 0)) { + REDEBUG("Decrypted new password blob claims length > 65536, " + "probably an invalid NT-Password"); + return -1; + } + + /* + * Sanity check - passlen positive and <= 512 if not, crypto has probably gone wrong + */ + if (passlen > 512) { + REDEBUG("Decrypted new password blob claims length %zu > 512, " + "probably an invalid NT-Password", passlen); + return -1; + } + + p = nt_pass_decrypted + 512 - passlen; + + /* + * The new NT hash - this should be preferred over the + * cleartext password as it avoids unicode hassles. + */ + new_hash = pair_make_request("MS-CHAP-New-NT-Password", NULL, T_OP_EQ); + new_hash->vp_length = NT_DIGEST_LENGTH; + new_hash->vp_octets = q = talloc_array(new_hash, uint8_t, new_hash->vp_length); + fr_md4_calc(q, p, passlen); + + /* + * Check that nt_password encrypted with new_hash + * matches the old_hash value from the client. + */ + smbhash(old_nt_hash_expected, nt_password->vp_octets, q); + smbhash(old_nt_hash_expected+8, nt_password->vp_octets+8, q + 7); + if (memcmp(old_nt_hash_expected, old_nt_hash, NT_DIGEST_LENGTH)!=0) { + REDEBUG("Old NT hash value from client does not match our value"); + return -1; + } + + /* + * The new cleartext password, which is utf-16 do some unpleasant vileness + * to turn it into utf8 without pulling in libraries like iconv. + * + * First pass: get the length of the converted string. + */ + new_pass = pair_make_request("MS-CHAP-New-Cleartext-Password", NULL, T_OP_EQ); + new_pass->vp_length = 0; + + i = 0; + while (i < passlen) { + int c; + + c = p[i++]; + c += p[i++] << 8; + + /* + * Gah. nasty. maybe we should just pull in iconv? + */ + if (c < 0x7f) { + new_pass->vp_length++; + } else if (c < 0x7ff) { + new_pass->vp_length += 2; + } else { + new_pass->vp_length += 3; + } + } + + new_pass->vp_strvalue = x = talloc_array(new_pass, char, new_pass->vp_length + 1); + + /* + * Second pass: convert the characters from UTF-16 to UTF-8. + */ + i = 0; + while (i < passlen) { + int c; + + c = p[i++]; + c += p[i++] << 8; + + /* + * Gah. nasty. maybe we should just pull in iconv? + */ + if (c < 0x7f) { + *x++ = c; + + } else if (c < 0x7ff) { + *x++ = 0xc0 + (c >> 6); + *x++ = 0x80 + (c & 0x3f); + + } else { + *x++ = 0xe0 + (c >> 12); + *x++ = 0x80 + ((c>>6) & 0x3f); + *x++ = 0x80 + (c & 0x3f); + } + } + + *x = '\0'; + + /* Perform the xlat */ + result_len = radius_xlat(result, sizeof(result), request, inst->local_cpw, NULL, NULL); + if (result_len < 0){ + return -1; + } else if (result_len == 0) { + REDEBUG("Local MS-CHAPv2 password change - xlat didn't give any result, assuming failure"); + return -1; + } + + RDEBUG("MS-CHAPv2 password change succeeded: %s", result); + + /* + * Update the NT-Password attribute with the new hash this lets us + * fall through to the authentication code using the new hash, + * not the old one. + */ + fr_pair_value_memcpy(nt_password, new_hash->vp_octets, new_hash->vp_length); + + /* + * Rock on! password change succeeded. + */ + return 0; +#else + REDEBUG("Local MS-CHAPv2 password changes require OpenSSL support"); + return -1; +#endif + } else { + REDEBUG("MS-CHAPv2 password change not configured"); + } + + return -1; +} + +/* + * Do the MS-CHAP stuff. + * + * This function is here so that all of the MS-CHAP related + * authentication is in one place, and we can perhaps later replace + * it with code to call winbindd, or something similar. + */ +static int CC_HINT(nonnull (1, 2, 4, 5 ,6)) do_mschap(rlm_mschap_t *inst, REQUEST *request, VALUE_PAIR *password, + uint8_t const *challenge, uint8_t const *response, + uint8_t nthashhash[NT_DIGEST_LENGTH], MSCHAP_AUTH_METHOD method) +{ + uint8_t calculated[24]; + + memset(nthashhash, 0, NT_DIGEST_LENGTH); + + switch (method) { + /* + * Do normal authentication. + */ + case AUTH_INTERNAL: + /* + * No password: can't do authentication. + */ + if (!password) { + REDEBUG("FAILED: No NT-Password. Cannot perform authentication"); + return -1; + } + + smbdes_mschap(password->vp_octets, challenge, calculated); + if (rad_digest_cmp(response, calculated, 24) != 0) { + return -1; + } + + /* + * If the password exists, and is an NT-Password, + * then calculate the hash of the NT hash. Doing this + * here minimizes work for later. + */ + if (!password->da->vendor && + (password->da->attr == PW_NT_PASSWORD)) { + fr_md4_calc(nthashhash, password->vp_octets, MD4_DIGEST_LENGTH); + } + break; + + /* + * Run ntlm_auth + */ + case AUTH_NTLMAUTH_EXEC: { + int result; + char buffer[256]; + size_t len; + + /* + * Run the program, and expect that we get 16 + */ + result = radius_exec_program(request, buffer, sizeof(buffer), NULL, request, inst->ntlm_auth, NULL, + true, true, inst->ntlm_auth_timeout); + if (result != 0) { + char *p; + + /* + * Do checks for numbers, which are + * language neutral. They're also + * faster. + */ + p = strcasestr(buffer, "0xC0000"); + if (p) { + int rcode = 0; + + p += 7; + if (strcmp(p, "224") == 0) { + rcode = -648; + + } else if (strcmp(p, "234") == 0) { + rcode = -647; + + } else if (strcmp(p, "072") == 0) { + rcode = -691; + + } else if (strcasecmp(p, "05E") == 0) { + rcode = -2; + } + + if (rcode != 0) { + REDEBUG2("%s", buffer); + return rcode; + } + + /* + * Else fall through to more ridiculous checks. + */ + } + + /* + * Look for variants of expire password. + */ + if (strcasestr(buffer, "0xC0000224") || + strcasestr(buffer, "Password expired") || + strcasestr(buffer, "Password has expired") || + strcasestr(buffer, "Password must be changed") || + strcasestr(buffer, "Must change password")) { + return -648; + } + + if (strcasestr(buffer, "0xC0000234") || + strcasestr(buffer, "Account locked out")) { + REDEBUG2("%s", buffer); + return -647; + } + + if (strcasestr(buffer, "0xC0000072") || + strcasestr(buffer, "Account disabled")) { + REDEBUG2("%s", buffer); + return -691; + } + + if (strcasestr(buffer, "0xC000005E") || + strcasestr(buffer, "No logon servers")) { + REDEBUG2("%s", buffer); + return -2; + } + + if (strcasestr(buffer, "could not obtain winbind separator") || + strcasestr(buffer, "Reading winbind reply failed")) { + REDEBUG2("%s", buffer); + return -2; + } + + RDEBUG2("External script failed"); + p = strchr(buffer, '\n'); + if (p) *p = '\0'; + + REDEBUG("External script says: %s", buffer); + return -1; + } + + /* + * Parse the answer as an nthashhash. + * + * ntlm_auth currently returns: + * NT_KEY: 000102030405060708090a0b0c0d0e0f + */ + if (memcmp(buffer, "NT_KEY: ", 8) != 0) { + REDEBUG("Invalid output from ntlm_auth: expecting 'NT_KEY: ' prefix"); + return -1; + } + + /* + * Check the length. It should be at least 32, with an LF at the end. + */ + len = strlen(buffer + 8); + if (len < 32) { + REDEBUG2("Invalid output from ntlm_auth: NT_KEY too short, expected 32 bytes got %zu bytes", + len); + + return -1; + } + + /* + * Update the NT hash hash, from the NT key. + */ + if (fr_hex2bin(nthashhash, NT_DIGEST_LENGTH, buffer + 8, len) != NT_DIGEST_LENGTH) { + REDEBUG("Invalid output from ntlm_auth: NT_KEY has non-hex values"); + return -1; + } + break; + } + +#ifdef WITH_AUTH_WINBIND + /* + * Process auth via the wbclient library + */ + case AUTH_WBCLIENT: + return do_auth_wbclient(inst, request, challenge, response, nthashhash); +#endif + + /* We should never reach this line */ + default: + RERROR("Internal error: Unknown mschap auth method (%d)", method); + return -1; + } + + return 0; +} + + +/* + * Data for the hashes. + */ +static const uint8_t SHSpad1[40] = + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + +static const uint8_t SHSpad2[40] = + { 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, + 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, + 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, + 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2 }; + +static const uint8_t magic1[27] = + { 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x4d, 0x50, 0x50, 0x45, 0x20, 0x4d, + 0x61, 0x73, 0x74, 0x65, 0x72, 0x20, 0x4b, 0x65, 0x79 }; + +static const uint8_t magic2[84] = + { 0x4f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x20, 0x73, 0x69, 0x64, 0x65, 0x2c, 0x20, + 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x73, 0x65, 0x6e, 0x64, 0x20, 0x6b, 0x65, 0x79, + 0x3b, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x73, 0x69, 0x64, 0x65, + 0x2c, 0x20, 0x69, 0x74, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x20, + 0x6b, 0x65, 0x79, 0x2e }; + +static const uint8_t magic3[84] = + { 0x4f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x20, 0x73, 0x69, 0x64, 0x65, 0x2c, 0x20, + 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x20, + 0x6b, 0x65, 0x79, 0x3b, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x73, + 0x69, 0x64, 0x65, 0x2c, 0x20, 0x69, 0x74, 0x20, 0x69, 0x73, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x6e, 0x64, 0x20, + 0x6b, 0x65, 0x79, 0x2e }; + + +static void mppe_GetMasterKey(uint8_t const *nt_hashhash,uint8_t const *nt_response, + uint8_t *masterkey) +{ + uint8_t digest[20]; + fr_sha1_ctx Context; + + fr_sha1_init(&Context); + fr_sha1_update(&Context,nt_hashhash,NT_DIGEST_LENGTH); + fr_sha1_update(&Context,nt_response,24); + fr_sha1_update(&Context,magic1,27); + fr_sha1_final(digest,&Context); + + memcpy(masterkey,digest,16); +} + + +static void mppe_GetAsymmetricStartKey(uint8_t *masterkey,uint8_t *sesskey, + int keylen,int issend) +{ + uint8_t digest[20]; + const uint8_t *s; + fr_sha1_ctx Context; + + memset(digest,0,20); + + if(issend) { + s = magic3; + } else { + s = magic2; + } + + fr_sha1_init(&Context); + fr_sha1_update(&Context,masterkey,16); + fr_sha1_update(&Context,SHSpad1,40); + fr_sha1_update(&Context,s,84); + fr_sha1_update(&Context,SHSpad2,40); + fr_sha1_final(digest,&Context); + + memcpy(sesskey,digest,keylen); +} + + +static void mppe_chap2_get_keys128(uint8_t const *nt_hashhash,uint8_t const *nt_response, + uint8_t *sendkey,uint8_t *recvkey) +{ + uint8_t masterkey[16]; + + mppe_GetMasterKey(nt_hashhash,nt_response,masterkey); + + mppe_GetAsymmetricStartKey(masterkey,sendkey,16,1); + mppe_GetAsymmetricStartKey(masterkey,recvkey,16,0); +} + +/* + * Generate MPPE keys. + */ +static void mppe_chap2_gen_keys128(uint8_t const *nt_hashhash,uint8_t const *response, + uint8_t *sendkey,uint8_t *recvkey) +{ + uint8_t enckey1[16]; + uint8_t enckey2[16]; + + mppe_chap2_get_keys128(nt_hashhash,response,enckey1,enckey2); + + /* + * dictionary.microsoft defines these attributes as + * 'encrypt=2'. The functions in src/lib/radius.c will + * take care of encrypting/decrypting them as appropriate, + * so that we don't have to. + */ + memcpy (sendkey, enckey1, 16); + memcpy (recvkey, enckey2, 16); +} + + +/* + * mod_authorize() - authorize user if we can authenticate + * it later. Add Auth-Type attribute if present in module + * configuration (usually Auth-Type must be "MS-CHAP") + */ +static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void * instance, REQUEST *request) +{ + rlm_mschap_t *inst = instance; + VALUE_PAIR *challenge = NULL; + + challenge = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT, TAG_ANY); + if (!challenge) { + return RLM_MODULE_NOOP; + } + + if (!fr_pair_find_by_num(request->packet->vps, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY) && + !fr_pair_find_by_num(request->packet->vps, PW_MSCHAP2_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY) && + !fr_pair_find_by_num(request->packet->vps, PW_MSCHAP2_CPW, VENDORPEC_MICROSOFT, TAG_ANY)) { + RDEBUG2("Found MS-CHAP-Challenge, but no MS-CHAP response or change-password"); + return RLM_MODULE_NOOP; + } + + if (fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY)) { + RWDEBUG2("Auth-Type already set. Not setting to MS-CHAP"); + return RLM_MODULE_NOOP; + } + + RDEBUG2("Found MS-CHAP attributes. Setting 'Auth-Type = %s'", inst->xlat_name); + + /* + * Set Auth-Type to MS-CHAP. The authentication code + * will take care of turning cleartext passwords into + * NT/LM passwords. + */ + if (!pair_make_config("Auth-Type", inst->auth_type, T_OP_EQ)) { + return RLM_MODULE_FAIL; + } + + return RLM_MODULE_OK; +} + +static rlm_rcode_t mschap_error(rlm_mschap_t *inst, REQUEST *request, unsigned char ident, + int mschap_result, int mschap_version, VALUE_PAIR *smb_ctrl) +{ + rlm_rcode_t rcode = RLM_MODULE_OK; + int error = 0; + int retry = 0; + char const *message = NULL; + + int i; + char new_challenge[33], buffer[128]; + char *p; + + if ((mschap_result == -648) || + ((mschap_result == 0) && + (smb_ctrl && ((smb_ctrl->vp_integer & ACB_PW_EXPIRED) != 0)))) { + REDEBUG("Password has expired. User should retry authentication"); + error = 648; + + /* + * A password change is NOT a retry! We MUST have retry=0 here. + */ + retry = 0; + message = "Password expired"; + rcode = RLM_MODULE_REJECT; + + /* + * Account is disabled. + * + * They're found, but they don't exist, so we + * return 'not found'. + */ + } else if ((mschap_result == -691) || + (smb_ctrl && (((smb_ctrl->vp_integer & ACB_DISABLED) != 0) || + ((smb_ctrl->vp_integer & (ACB_NORMAL|ACB_WSTRUST)) == 0)))) { + REDEBUG("SMB-Account-Ctrl (or ntlm_auth) " + "says that the account is disabled, " + "or is not a normal or workstation trust account"); + error = 691; + retry = 0; + message = "Account disabled"; + rcode = RLM_MODULE_NOTFOUND; + + /* + * User is locked out. + */ + } else if ((mschap_result == -647) || + (smb_ctrl && ((smb_ctrl->vp_integer & ACB_AUTOLOCK) != 0))) { + REDEBUG("SMB-Account-Ctrl (or ntlm_auth) " + "says that the account is locked out"); + error = 647; + retry = 0; + message = "Account locked out"; + rcode = RLM_MODULE_USERLOCK; + } else if (mschap_result == -2) { + RDEBUG("Authentication failed"); + error = 691; + retry = inst->allow_retry; + message = "Authentication failed"; + rcode = RLM_MODULE_FAIL; + + } else if (mschap_result < 0) { + REDEBUG("MS-CHAP2-Response is incorrect"); + error = 691; + retry = inst->allow_retry; + message = "Authentication rejected"; + rcode = RLM_MODULE_REJECT; + } + + if (rcode == RLM_MODULE_OK) return RLM_MODULE_OK; + + switch (mschap_version) { + case 1: + for (p = new_challenge, i = 0; i < 2; i++) p += snprintf(p, 9, "%08x", fr_rand()); + snprintf(buffer, sizeof(buffer), "E=%i R=%i C=%s V=2", + error, retry, new_challenge); + break; + + case 2: + for (p = new_challenge, i = 0; i < 4; i++) p += snprintf(p, 9, "%08x", fr_rand()); + snprintf(buffer, sizeof(buffer), "E=%i R=%i C=%s V=3 M=%s", + error, retry, new_challenge, message); + break; + + default: + return RLM_MODULE_FAIL; + } + mschap_add_reply(request, ident, "MS-CHAP-Error", buffer, strlen(buffer)); + + return rcode; +} + +/* + * mod_authenticate() - authenticate user based on given + * attributes and configuration. + * We will try to find out password in configuration + * or in configured passwd file. + * If one is found we will check paraneters given by NAS. + * + * If PW_SMB_ACCOUNT_CTRL is not set to ACB_PWNOTREQ we must have + * one of: + * PAP: PW_USER_PASSWORD or + * MS-CHAP: PW_MSCHAP_CHALLENGE and PW_MSCHAP_RESPONSE or + * MS-CHAP2: PW_MSCHAP_CHALLENGE and PW_MSCHAP2_RESPONSE + * In case of password mismatch or locked account we MAY return + * PW_MSCHAP_ERROR for MS-CHAP or MS-CHAP v2 + * If MS-CHAP2 succeeds we MUST return + * PW_MSCHAP2_SUCCESS + */ +static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request) +{ + rlm_mschap_t *inst = instance; + VALUE_PAIR *challenge = NULL; + VALUE_PAIR *response = NULL; + VALUE_PAIR *cpw = NULL; + VALUE_PAIR *password = NULL; + VALUE_PAIR *nt_password, *smb_ctrl; + VALUE_PAIR *username; + uint8_t nthashhash[NT_DIGEST_LENGTH]; + char msch2resp[42]; + char const *username_string; + int mschap_version = 0; + int mschap_result; + MSCHAP_AUTH_METHOD auth_method; + + /* + * If we have ntlm_auth configured, use it unless told + * otherwise + */ + auth_method = inst->method; + + /* + * If we have an ntlm_auth configuration, then we may + * want to suppress it. + */ + if (auth_method != AUTH_INTERNAL) { + VALUE_PAIR *vp = fr_pair_find_by_num(request->config, PW_MS_CHAP_USE_NTLM_AUTH, 0, TAG_ANY); + if (vp && vp->vp_integer == 0) auth_method = AUTH_INTERNAL; + } + + /* + * Find the SMB-Account-Ctrl attribute, or the + * SMB-Account-Ctrl-Text attribute. + */ + smb_ctrl = fr_pair_find_by_num(request->config, PW_SMB_ACCOUNT_CTRL, 0, TAG_ANY); + if (!smb_ctrl) { + password = fr_pair_find_by_num(request->config, PW_SMB_ACCOUNT_CTRL_TEXT, 0, TAG_ANY); + if (password) { + smb_ctrl = pair_make_config("SMB-Account-CTRL", "0", T_OP_SET); + if (smb_ctrl) { + smb_ctrl->vp_integer = pdb_decode_acct_ctrl(password->vp_strvalue); + } + } + } + + /* + * We're configured to do MS-CHAP authentication. + * and account control information exists. Enforce it. + */ + if (smb_ctrl) { + /* + * Password is not required. + */ + if ((smb_ctrl->vp_integer & ACB_PWNOTREQ) != 0) { + RDEBUG2("SMB-Account-Ctrl says no password is required"); + return RLM_MODULE_OK; + } + } + + /* + * Decide how to get the passwords. + */ + password = fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY); + + /* + * We need an NT-Password. + */ + nt_password = fr_pair_find_by_num(request->config, PW_NT_PASSWORD, 0, TAG_ANY); + if (nt_password) { + VERIFY_VP(nt_password); + + switch (nt_password->vp_length) { + case NT_DIGEST_LENGTH: + RDEBUG2("Found NT-Password"); + break; + + /* 0x */ + case 34: + case 32: + RWDEBUG("NT-Password has not been normalized by the 'pap' module (likely still in hex format). " + "Authentication may fail"); + nt_password = NULL; + break; + + default: + RWDEBUG("NT-Password found but incorrect length, expected " STRINGIFY(NT_DIGEST_LENGTH) + " bytes got %zu bytes. Authentication may fail", nt_password->vp_length); + nt_password = NULL; + break; + } + } + + /* + * ... or a Cleartext-Password, which we now transform into an NT-Password + */ + if (!nt_password) { + uint8_t *p; + + if (password) { + RDEBUG2("Found Cleartext-Password, hashing to create NT-Password"); + nt_password = pair_make_config("NT-Password", NULL, T_OP_EQ); + if (!nt_password) { + RERROR("No memory"); + return RLM_MODULE_FAIL; + } + nt_password->vp_length = NT_DIGEST_LENGTH; + nt_password->vp_octets = p = talloc_array(nt_password, uint8_t, nt_password->vp_length); + + if (mschap_ntpwdhash(p, password->vp_strvalue) < 0) { + RERROR("Failed generating NT-Password"); + return RLM_MODULE_FAIL; + } + } else if (auth_method == AUTH_INTERNAL) { + RWDEBUG2("No Cleartext-Password configured. Cannot create NT-Password"); + } + } + + cpw = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP2_CPW, VENDORPEC_MICROSOFT, TAG_ANY); + if (cpw) { + /* + * mschap2 password change request + * we cheat - first decode and execute the passchange + * we then extract the response, add it into the request + * then jump into mschap2 auth with the chal/resp + */ + uint8_t new_nt_encrypted[516], old_nt_encrypted[NT_DIGEST_LENGTH]; + VALUE_PAIR *nt_enc=NULL; + int seq, new_nt_enc_len; + uint8_t *p; + + RDEBUG("MS-CHAPv2 password change request received"); + + if (cpw->vp_length != 68) { + REDEBUG("MS-CHAP2-CPW has the wrong format: length %zu != 68", cpw->vp_length); + return RLM_MODULE_INVALID; + } + + if (cpw->vp_octets[0] != 7) { + REDEBUG("MS-CHAP2-CPW has the wrong format: code %d != 7", cpw->vp_octets[0]); + return RLM_MODULE_INVALID; + } + + /* + * look for the new (encrypted) password + * bah stupid composite attributes + * we're expecting 3 attributes with the leading bytes + * 06::00:01:<1st chunk> + * 06::00:02:<2nd chunk> + * 06::00:03:<3rd chunk> + */ + new_nt_enc_len = 0; + for (seq = 1; seq < 4; seq++) { + vp_cursor_t cursor; + int found = 0; + + for (nt_enc = fr_cursor_init(&cursor, &request->packet->vps); + nt_enc; + nt_enc = fr_cursor_next(&cursor)) { + if (nt_enc->da->vendor != VENDORPEC_MICROSOFT) + continue; + + if (nt_enc->da->attr != PW_MSCHAP_NT_ENC_PW) + continue; + + if (nt_enc->vp_length < 4) { + REDEBUG("MS-CHAP-NT-Enc-PW with invalid format"); + return RLM_MODULE_INVALID; + } + + if (nt_enc->vp_octets[0] != 6) { + REDEBUG("MS-CHAP-NT-Enc-PW with invalid format"); + return RLM_MODULE_INVALID; + } + + if ((nt_enc->vp_octets[2] == 0) && (nt_enc->vp_octets[3] == seq)) { + found = 1; + break; + } + } + + if (!found) { + REDEBUG("Could not find MS-CHAP-NT-Enc-PW w/ sequence number %d", seq); + return RLM_MODULE_INVALID; + } + + if ((new_nt_enc_len + nt_enc->vp_length - 4) > sizeof(new_nt_encrypted)) { + REDEBUG("Unpacked MS-CHAP-NT-Enc-PW length > 516"); + return RLM_MODULE_INVALID; + } + + memcpy(new_nt_encrypted + new_nt_enc_len, nt_enc->vp_octets + 4, nt_enc->vp_length - 4); + new_nt_enc_len += nt_enc->vp_length - 4; + } + + if (new_nt_enc_len != 516) { + REDEBUG("Unpacked MS-CHAP-NT-Enc-PW length != 516"); + return RLM_MODULE_INVALID; + } + + /* + * RFC 2548 is confusing here + * it claims: + * + * 1 byte code + * 1 byte ident + * 16 octets - old hash encrypted with new hash + * 24 octets - peer challenge + * this is actually: + * 16 octets - peer challenge + * 8 octets - reserved + * 24 octets - nt response + * 2 octets - flags (ignored) + */ + + memcpy(old_nt_encrypted, cpw->vp_octets + 2, sizeof(old_nt_encrypted)); + + RDEBUG2("Password change payload valid"); + + /* perform the actual password change */ + if (do_mschap_cpw(inst, request, nt_password, new_nt_encrypted, old_nt_encrypted, auth_method) < 0) { + char buffer[128]; + + REDEBUG("Password change failed"); + + snprintf(buffer, sizeof(buffer), "E=709 R=0 M=Password change failed"); + mschap_add_reply(request, cpw->vp_octets[1], "MS-CHAP-Error", buffer, strlen(buffer)); + + return RLM_MODULE_REJECT; + } + RDEBUG("Password change successful"); + + /* + * Clear any expiry bit so the user can now login; + * obviously the password change action will need + * to have cleared this bit in the config/SQL/wherever + */ + if (smb_ctrl && smb_ctrl->vp_integer & ACB_PW_EXPIRED) { + RDEBUG("Clearing expiry bit in SMB-Acct-Ctrl to allow authentication"); + smb_ctrl->vp_integer &= ~ACB_PW_EXPIRED; + } + + /* + * Extract the challenge & response from the end of the password + * change, add them into the request and then continue with + * the authentication + */ + response = radius_pair_create(request->packet, &request->packet->vps, + PW_MSCHAP2_RESPONSE, + VENDORPEC_MICROSOFT); + response->vp_length = 50; + response->vp_octets = p = talloc_array(response, uint8_t, response->vp_length); + + /* ident & flags */ + p[0] = cpw->vp_octets[1]; + p[1] = 0; + /* peer challenge and client NT response */ + memcpy(p + 2, cpw->vp_octets + 18, 48); + } + + challenge = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT, TAG_ANY); + if (!challenge) { + REDEBUG("You set 'Auth-Type = MS-CHAP' for a request that does not contain any MS-CHAP attributes!"); + return RLM_MODULE_REJECT; + } + + /* + * We also require an MS-CHAP-Response. + */ + response = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY); + + /* + * MS-CHAP-Response, means MS-CHAPv1 + */ + if (response) { + int offset; + rlm_rcode_t rcode; + mschap_version = 1; + + /* + * MS-CHAPv1 challenges are 8 octets. + */ + if (challenge->vp_length < 8) { + REDEBUG("MS-CHAP-Challenge has the wrong format"); + return RLM_MODULE_INVALID; + } + + /* + * Responses are 50 octets. + */ + if (response->vp_length < 50) { + REDEBUG("MS-CHAP-Response has the wrong format"); + return RLM_MODULE_INVALID; + } + + /* + * We are doing MS-CHAP. Calculate the MS-CHAP + * response + */ + if (response->vp_octets[1] & 0x01) { + RDEBUG2("Client is using MS-CHAPv1 with NT-Password"); + password = nt_password; + offset = 26; + } else { + REDEBUG2("Client is using MS-CHAPv1 with unsupported method LM-Password"); + return RLM_MODULE_FAIL; + } + + /* + * Do the MS-CHAP authentication. + */ + mschap_result = do_mschap(inst, request, password, challenge->vp_octets, + response->vp_octets + offset, nthashhash, auth_method); + /* + * Check for errors, and add MSCHAP-Error if necessary. + */ + rcode = mschap_error(inst, request, *response->vp_octets, + mschap_result, mschap_version, smb_ctrl); + if (rcode != RLM_MODULE_OK) return rcode; + } else if ((response = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP2_RESPONSE, + VENDORPEC_MICROSOFT, TAG_ANY)) != NULL) { + uint8_t mschapv1_challenge[16]; + VALUE_PAIR *name_attr, *response_name, *peer_challenge_attr; + rlm_rcode_t rcode; + uint8_t const *peer_challenge; + + mschap_version = 2; + + /* + * MS-CHAPv2 challenges are 16 octets. + */ + if (challenge->vp_length < 16) { + REDEBUG("MS-CHAP-Challenge has the wrong format"); + return RLM_MODULE_INVALID; + } + + /* + * Responses are 50 octets. + */ + if (response->vp_length < 50) { + REDEBUG("MS-CHAP-Response has the wrong format"); + return RLM_MODULE_INVALID; + } + + /* + * We also require a User-Name + */ + username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + if (!username) { + REDEBUG("We require a User-Name for MS-CHAPv2"); + return RLM_MODULE_INVALID; + } + + /* + * Check for MS-CHAP-User-Name and if found, use it + * to construct the MSCHAPv1 challenge. This is + * set by rlm_eap_mschap to the MS-CHAP Response + * packet Name field. + * + * We prefer this to the User-Name in the + * packet. + */ + response_name = fr_pair_find_by_num(request->packet->vps, PW_MS_CHAP_USER_NAME, 0, TAG_ANY); + if (response_name) { + name_attr = response_name; + } else { + name_attr = username; + } + + /* + * with_ntdomain_hack moved here, too. + */ + if ((username_string = strchr(name_attr->vp_strvalue, '\\')) != NULL) { + if (inst->with_ntdomain_hack) { + username_string++; + } else { + RWDEBUG2("NT Domain delimiter found, should with_ntdomain_hack of been enabled?"); + username_string = name_attr->vp_strvalue; + } + } else { + username_string = name_attr->vp_strvalue; + } + + if (response_name && ((username->vp_length != response_name->vp_length) || + (strncasecmp(username->vp_strvalue, response_name->vp_strvalue, username->vp_length) != 0))) { + RWDEBUG("User-Name (%s) is not the same as MS-CHAP Name (%s) from EAP-MSCHAPv2", + username->vp_strvalue, response_name->vp_strvalue); + } + +#ifdef __APPLE__ + /* + * No "known good" NT-Password attribute. Try to do + * OpenDirectory authentication. + * + * If OD determines the user is an OD user it will return noop, which + * indicates the auth process should continue directly to OD. + * Otherwise OD will determine auth success/fail. + */ + if (!nt_password && inst->open_directory) { + RDEBUG2("No NT-Password configured. Trying OpenDirectory Authentication"); + int odStatus = od_mschap_auth(request, challenge, username); + if (odStatus != RLM_MODULE_NOOP) { + return odStatus; + } + } +#endif + peer_challenge = response->vp_octets + 2; + + peer_challenge_attr = fr_pair_find_by_num(request->config, PW_MS_CHAP_PEER_CHALLENGE, 0, TAG_ANY); + if (peer_challenge_attr) { + RDEBUG2("Overriding peer challenge"); + peer_challenge = peer_challenge_attr->vp_octets; + } + + /* + * The old "mschapv2" function has been moved to + * here. + * + * MS-CHAPv2 takes some additional data to create an + * MS-CHAPv1 challenge, and then does MS-CHAPv1. + */ + RDEBUG2("Creating challenge hash with username: %s", username_string); + mschap_challenge_hash(peer_challenge, /* peer challenge */ + challenge->vp_octets, /* our challenge */ + username_string, /* user name */ + mschapv1_challenge); /* resulting challenge */ + + RDEBUG2("Client is using MS-CHAPv2"); + mschap_result = do_mschap(inst, request, nt_password, mschapv1_challenge, + response->vp_octets + 26, nthashhash, auth_method); + rcode = mschap_error(inst, request, *response->vp_octets, + mschap_result, mschap_version, smb_ctrl); + if (rcode != RLM_MODULE_OK) return rcode; + +#ifdef WITH_AUTH_WINBIND + if (inst->wb_retry_with_normalised_username) { + if ((response_name = fr_pair_find_by_num(request->packet->vps, PW_MS_CHAP_USER_NAME, 0, TAG_ANY))) { + if (strcmp(username_string, response_name->vp_strvalue)) { + RDEBUG2("Changing username %s to %s", username_string, response_name->vp_strvalue); + username_string = response_name->vp_strvalue; + } + } + } +#endif + + mschap_auth_response(username_string, /* without the domain */ + nthashhash, /* nt-hash-hash */ + response->vp_octets + 26, /* peer response */ + peer_challenge, /* peer challenge */ + challenge->vp_octets, /* our challenge */ + msch2resp); /* calculated MPPE key */ + mschap_add_reply(request, *response->vp_octets, "MS-CHAP2-Success", msch2resp, 42); + + } else { /* Neither CHAPv1 or CHAPv2 response: die */ + REDEBUG("You set 'Auth-Type = MS-CHAP' for a request that does not contain any MS-CHAP attributes!"); + return RLM_MODULE_INVALID; + } + + /* now create MPPE attributes */ + if (inst->use_mppe) { + uint8_t mppe_sendkey[34]; + uint8_t mppe_recvkey[34]; + + if (mschap_version == 1) { + RDEBUG2("adding MS-CHAPv1 MPPE keys"); + memset(mppe_sendkey, 0, 32); + + /* + * According to RFC 2548 we + * should send NT hash. But in + * practice it doesn't work. + * Instead, we should send nthashhash + * + * This is an error in RFC 2548. + */ + /* + * do_mschap cares to zero nthashhash if NT hash + * is not available. + */ + memcpy(mppe_sendkey + 8, nthashhash, NT_DIGEST_LENGTH); + mppe_add_reply(request, "MS-CHAP-MPPE-Keys", mppe_sendkey, 24); + + } else if (mschap_version == 2) { + RDEBUG2("Adding MS-CHAPv2 MPPE keys"); + mppe_chap2_gen_keys128(nthashhash, response->vp_octets + 26, mppe_sendkey, mppe_recvkey); + + mppe_add_reply(request, "MS-MPPE-Recv-Key", mppe_recvkey, 16); + mppe_add_reply(request, "MS-MPPE-Send-Key", mppe_sendkey, 16); + + } + pair_make_reply("MS-MPPE-Encryption-Policy", + (inst->require_encryption) ? "0x00000002":"0x00000001", T_OP_EQ); + pair_make_reply("MS-MPPE-Encryption-Types", + (inst->require_strong) ? "0x00000004":"0x00000006", T_OP_EQ); + } /* else we weren't asked to use MPPE */ + + return RLM_MODULE_OK; +#undef inst +} + +extern module_t rlm_mschap; +module_t rlm_mschap = { + .magic = RLM_MODULE_INIT, + .name = "mschap", + .type = 0, + .inst_size = sizeof(rlm_mschap_t), + .config = module_config, + .bootstrap = mod_bootstrap, + .instantiate = mod_instantiate, + .detach = mod_detach, + .methods = { + [MOD_AUTHENTICATE] = mod_authenticate, + [MOD_AUTHORIZE] = mod_authorize + }, +}; -- cgit v1.2.3