From c0fc6a6d7f7a9d709f35c1a7e4812c0a89285977 Mon Sep 17 00:00:00 2001 From: Stefan Metzmacher Date: Thu, 19 Nov 2015 16:02:58 +0100 Subject: CVE-2016-2110: auth/ntlmssp: implement new_spnego support including MIC checking (as server) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now include a MsvAvTimestamp in our target info as indication for the client to include a NTLMSSP_MIC in the AUTH_MESSAGE. If the client uses NTLMv2 we check NTLMSSP_AVFLAG_MIC_IN_AUTHENTICATE_MESSAGE and require a valid MIC. This is still disabled if the "map to guest" feature is used. BUG: https://bugzilla.samba.org/show_bug.cgi?id=11644 Signed-off-by: Stefan Metzmacher Reviewed-by: Günther Deschner --- auth/ntlmssp/gensec_ntlmssp.c | 9 + auth/ntlmssp/gensec_ntlmssp_server.c | 23 ++- auth/ntlmssp/ntlmssp.h | 6 + auth/ntlmssp/ntlmssp_server.c | 341 ++++++++++++++++++++++++++++++++++- 4 files changed, 367 insertions(+), 12 deletions(-) (limited to 'auth/ntlmssp') diff --git a/auth/ntlmssp/gensec_ntlmssp.c b/auth/ntlmssp/gensec_ntlmssp.c index 567258914af..329d8eb4751 100644 --- a/auth/ntlmssp/gensec_ntlmssp.c +++ b/auth/ntlmssp/gensec_ntlmssp.c @@ -105,6 +105,15 @@ bool gensec_ntlmssp_have_feature(struct gensec_security *gensec_security, if (feature & GENSEC_FEATURE_SIGN_PKT_HEADER) { return true; } + if (feature & GENSEC_FEATURE_NEW_SPNEGO) { + if (!ntlmssp_state->session_key.length) { + return false; + } + if (!(ntlmssp_state->neg_flags & NTLMSSP_NEGOTIATE_SIGN)) { + return false; + } + return ntlmssp_state->new_spnego; + } return false; } diff --git a/auth/ntlmssp/gensec_ntlmssp_server.c b/auth/ntlmssp/gensec_ntlmssp_server.c index 1a8e66e63e2..ca19863eadd 100644 --- a/auth/ntlmssp/gensec_ntlmssp_server.c +++ b/auth/ntlmssp/gensec_ntlmssp_server.c @@ -34,9 +34,9 @@ #include "auth/gensec/gensec_internal.h" #include "auth/common_auth.h" #include "param/param.h" +#include "param/loadparm.h" #include "libds/common/roles.h" - /** * Return the credentials of a logged on user, including session keys * etc. @@ -99,6 +99,9 @@ NTSTATUS gensec_ntlmssp_server_start(struct gensec_security *gensec_security) const char *netbios_domain; const char *dns_name; const char *dns_domain; + enum server_role role; + + role = lpcfg_server_role(gensec_security->settings->lp_ctx); nt_status = gensec_ntlmssp_start(gensec_security); NT_STATUS_NOT_OK_RETURN(nt_status); @@ -128,6 +131,22 @@ NTSTATUS gensec_ntlmssp_server_start(struct gensec_security *gensec_security) ntlmssp_state->allow_lm_key = true; } + if (lpcfg_map_to_guest(gensec_security->settings->lp_ctx) != NEVER_MAP_TO_GUEST) { + /* + * map to guest is not secure anyway, so + * try to make it work and don't try to + * negotiate new_spnego and MIC checking + */ + ntlmssp_state->force_old_spnego = true; + } + + if (role == ROLE_ACTIVE_DIRECTORY_DC) { + /* + * map to guest is not supported on an AD DC. + */ + ntlmssp_state->force_old_spnego = false; + } + ntlmssp_state->neg_flags = NTLMSSP_NEGOTIATE_NTLM | NTLMSSP_NEGOTIATE_VERSION; @@ -175,7 +194,7 @@ NTSTATUS gensec_ntlmssp_server_start(struct gensec_security *gensec_security) ntlmssp_state->neg_flags |= NTLMSSP_NEGOTIATE_SEAL; } - if (lpcfg_server_role(gensec_security->settings->lp_ctx) == ROLE_STANDALONE) { + if (role == ROLE_STANDALONE) { ntlmssp_state->server.is_standalone = true; } else { ntlmssp_state->server.is_standalone = false; diff --git a/auth/ntlmssp/ntlmssp.h b/auth/ntlmssp/ntlmssp.h index bb8807df425..bc2a8225386 100644 --- a/auth/ntlmssp/ntlmssp.h +++ b/auth/ntlmssp/ntlmssp.h @@ -72,6 +72,11 @@ struct ntlmssp_state uint8_t *nt_hash; uint8_t *lm_hash; + DATA_BLOB negotiate_blob; + DATA_BLOB challenge_blob; + bool new_spnego; + bool force_old_spnego; + struct { const char *netbios_name; const char *netbios_domain; @@ -83,6 +88,7 @@ struct ntlmssp_state const char *netbios_domain; const char *dns_name; const char *dns_domain; + NTTIME challenge_endtime; struct AV_PAIR_LIST av_pair_list; } server; diff --git a/auth/ntlmssp/ntlmssp_server.c b/auth/ntlmssp/ntlmssp_server.c index 7013df78329..17d5adeaeb5 100644 --- a/auth/ntlmssp/ntlmssp_server.c +++ b/auth/ntlmssp/ntlmssp_server.c @@ -21,6 +21,7 @@ */ #include "includes.h" +#include "lib/util/time_basic.h" #include "auth/ntlmssp/ntlmssp.h" #include "auth/ntlmssp/ntlmssp_private.h" #include "../librpc/gen_ndr/ndr_ntlmssp.h" @@ -84,6 +85,27 @@ NTSTATUS gensec_ntlmssp_server_negotiate(struct gensec_security *gensec_security uint8_t cryptkey[8]; const char *target_name; NTSTATUS status; + struct timeval tv_now = timeval_current(); + /* + * See [MS-NLMP] + * + * Windows NT 4.0, windows_2000: use 30 minutes, + * Windows XP, Windows Server 2003, Windows Vista, + * Windows Server 2008, Windows 7, and Windows Server 2008 R2 + * use 36 hours. + * + * Newer systems doesn't check this, likely because the + * connectionless NTLMSSP is no longer supported. + * + * As we expect the AUTHENTICATION_MESSAGE to arrive + * directly after the NEGOTIATE_MESSAGE (typically less than + * as 1 second later). We use a hard timeout of 30 Minutes. + * + * We don't look at AUTHENTICATE_MESSAGE.NtChallengeResponse.TimeStamp + * instead we just remember our own time. + */ + uint32_t max_lifetime = 30 * 60; + struct timeval tv_end = timeval_add(&tv_now, max_lifetime, 0); /* parse the NTLMSSP packet */ #if 0 @@ -91,6 +113,12 @@ NTSTATUS gensec_ntlmssp_server_negotiate(struct gensec_security *gensec_security #endif if (request.length) { + if (request.length > UINT16_MAX) { + DEBUG(1, ("ntlmssp_server_negotiate: reject large request of length %u\n", + (unsigned int)request.length)); + return NT_STATUS_INVALID_PARAMETER; + } + if ((request.length < 16) || !msrpc_parse(ntlmssp_state, &request, "Cdd", "NTLMSSP", &ntlmssp_command, @@ -141,6 +169,7 @@ NTSTATUS gensec_ntlmssp_server_negotiate(struct gensec_security *gensec_security */ chal_flags = ntlmssp_state->neg_flags; + ntlmssp_state->server.challenge_endtime = timeval_to_nttime(&tv_end); /* get the right name to fill in as 'target' */ target_name = ntlmssp_target_name(ntlmssp_state, @@ -158,7 +187,7 @@ NTSTATUS gensec_ntlmssp_server_negotiate(struct gensec_security *gensec_security struct AV_PAIR *pairs = NULL; uint32_t count = 5; - pairs = talloc_zero_array(ntlmssp_state, struct AV_PAIR, count); + pairs = talloc_zero_array(ntlmssp_state, struct AV_PAIR, count + 1); if (pairs == NULL) { return NT_STATUS_NO_MEMORY; } @@ -175,7 +204,16 @@ NTSTATUS gensec_ntlmssp_server_negotiate(struct gensec_security *gensec_security pairs[3].AvId = MsvAvDnsComputerName; pairs[3].Value.AvDnsComputerName= ntlmssp_state->server.dns_name; - pairs[4].AvId = MsvAvEOL; + if (!ntlmssp_state->force_old_spnego) { + pairs[4].AvId = MsvAvTimestamp; + pairs[4].Value.AvTimestamp = + timeval_to_nttime(&tv_now); + count += 1; + + pairs[5].AvId = MsvAvEOL; + } else { + pairs[4].AvId = MsvAvEOL; + } ntlmssp_state->server.av_pair_list.count = count; ntlmssp_state->server.av_pair_list.pair = pairs; @@ -235,6 +273,18 @@ NTSTATUS gensec_ntlmssp_server_negotiate(struct gensec_security *gensec_security data_blob_free(&struct_blob); + ntlmssp_state->negotiate_blob = data_blob_dup_talloc(ntlmssp_state, + request); + if (ntlmssp_state->negotiate_blob.length != request.length) { + return NT_STATUS_NO_MEMORY; + } + + ntlmssp_state->challenge_blob = data_blob_dup_talloc(ntlmssp_state, + *reply); + if (ntlmssp_state->challenge_blob.length != reply->length) { + return NT_STATUS_NO_MEMORY; + } + ntlmssp_state->expected_state = NTLMSSP_AUTH; return NT_STATUS_MORE_PROCESSING_REQUIRED; @@ -267,19 +317,24 @@ static NTSTATUS ntlmssp_server_preauth(struct gensec_security *gensec_security, struct auth4_context *auth_context = gensec_security->auth_context; uint32_t ntlmssp_command, auth_flags; NTSTATUS nt_status; - + const unsigned int version_len = 8; + DATA_BLOB version_blob = data_blob_null; + const unsigned int mic_len = NTLMSSP_MIC_SIZE; + DATA_BLOB mic_blob = data_blob_null; uint8_t session_nonce_hash[16]; - const char *parse_string; + bool ok; + struct timeval endtime; + bool expired = false; #if 0 file_save("ntlmssp_auth.dat", request.data, request.length); #endif if (ntlmssp_state->unicode) { - parse_string = "CdBBUUUBd"; + parse_string = "CdBBUUUBdbb"; } else { - parse_string = "CdBBAAABd"; + parse_string = "CdBBAAABdbb"; } /* zero these out */ @@ -292,7 +347,7 @@ static NTSTATUS ntlmssp_server_preauth(struct gensec_security *gensec_security, ntlmssp_state->client.netbios_name = NULL; /* now the NTLMSSP encoded auth hashes */ - if (!msrpc_parse(ntlmssp_state, &request, parse_string, + ok = msrpc_parse(ntlmssp_state, &request, parse_string, "NTLMSSP", &ntlmssp_command, &ntlmssp_state->lm_resp, @@ -301,7 +356,35 @@ static NTSTATUS ntlmssp_server_preauth(struct gensec_security *gensec_security, &ntlmssp_state->user, &ntlmssp_state->client.netbios_name, &state->encrypted_session_key, - &auth_flags)) { + &auth_flags, + &version_blob, version_len, + &mic_blob, mic_len); + if (!ok) { + DEBUG(10, ("ntlmssp_server_auth: failed to parse NTLMSSP (nonfatal):\n")); + dump_data(10, request.data, request.length); + + data_blob_free(&version_blob); + data_blob_free(&mic_blob); + + if (ntlmssp_state->unicode) { + parse_string = "CdBBUUUBd"; + } else { + parse_string = "CdBBAAABd"; + } + + ok = msrpc_parse(ntlmssp_state, &request, parse_string, + "NTLMSSP", + &ntlmssp_command, + &ntlmssp_state->lm_resp, + &ntlmssp_state->nt_resp, + &ntlmssp_state->domain, + &ntlmssp_state->user, + &ntlmssp_state->client.netbios_name, + &state->encrypted_session_key, + &auth_flags); + } + + if (!ok) { DEBUG(10, ("ntlmssp_server_auth: failed to parse NTLMSSP (nonfatal):\n")); dump_data(10, request.data, request.length); @@ -370,6 +453,194 @@ static NTSTATUS ntlmssp_server_preauth(struct gensec_security *gensec_security, file_save("lmhash1.dat", &ntlmssp_state->lm_resp.data, &ntlmssp_state->lm_resp.length); #endif + if (ntlmssp_state->nt_resp.length > 24) { + struct NTLMv2_RESPONSE v2_resp; + enum ndr_err_code err; + uint32_t i = 0; + uint32_t count = 0; + const struct AV_PAIR *flags = NULL; + const struct AV_PAIR *eol = NULL; + uint32_t av_flags = 0; + + err = ndr_pull_struct_blob(&ntlmssp_state->nt_resp, + ntlmssp_state, + &v2_resp, + (ndr_pull_flags_fn_t)ndr_pull_NTLMv2_RESPONSE); + if (!NDR_ERR_CODE_IS_SUCCESS(err)) { + nt_status = ndr_map_error2ntstatus(err); + DEBUG(1,("%s: failed to parse NTLMv2_RESPONSE of length %zu for " + "user=[%s] domain=[%s] workstation=[%s] - %s %s\n", + __func__, ntlmssp_state->nt_resp.length, + ntlmssp_state->user, ntlmssp_state->domain, + ntlmssp_state->client.netbios_name, + ndr_errstr(err), nt_errstr(nt_status))); + return nt_status; + } + + if (DEBUGLVL(10)) { + NDR_PRINT_DEBUG(NTLMv2_RESPONSE, &v2_resp); + } + + eol = ndr_ntlmssp_find_av(&v2_resp.Challenge.AvPairs, + MsvAvEOL); + if (eol == NULL) { + DEBUG(1,("%s: missing MsvAvEOL for " + "user=[%s] domain=[%s] workstation=[%s]\n", + __func__, ntlmssp_state->user, ntlmssp_state->domain, + ntlmssp_state->client.netbios_name)); + return NT_STATUS_INVALID_PARAMETER; + } + + flags = ndr_ntlmssp_find_av(&v2_resp.Challenge.AvPairs, + MsvAvFlags); + if (flags != NULL) { + av_flags = flags->Value.AvFlags; + } + + if (av_flags & NTLMSSP_AVFLAG_MIC_IN_AUTHENTICATE_MESSAGE) { + if (mic_blob.length != NTLMSSP_MIC_SIZE) { + DEBUG(1,("%s: mic_blob.length[%u] for " + "user=[%s] domain=[%s] workstation=[%s]\n", + __func__, + (unsigned)mic_blob.length, + ntlmssp_state->user, + ntlmssp_state->domain, + ntlmssp_state->client.netbios_name)); + return NT_STATUS_INVALID_PARAMETER; + } + + if (request.length < + (NTLMSSP_MIC_OFFSET + NTLMSSP_MIC_SIZE)) + { + DEBUG(1,("%s: missing MIC " + "request.length[%u] for " + "user=[%s] domain=[%s] workstation=[%s]\n", + __func__, + (unsigned)request.length, + ntlmssp_state->user, + ntlmssp_state->domain, + ntlmssp_state->client.netbios_name)); + return NT_STATUS_INVALID_PARAMETER; + } + + ntlmssp_state->new_spnego = true; + } + + count = ntlmssp_state->server.av_pair_list.count; + if (v2_resp.Challenge.AvPairs.count < count) { + return NT_STATUS_INVALID_PARAMETER; + } + + for (i = 0; i < count; i++) { + const struct AV_PAIR *sp = + &ntlmssp_state->server.av_pair_list.pair[i]; + const struct AV_PAIR *cp = NULL; + + if (sp->AvId == MsvAvEOL) { + continue; + } + + cp = ndr_ntlmssp_find_av(&v2_resp.Challenge.AvPairs, + sp->AvId); + if (cp == NULL) { + DEBUG(1,("%s: AvId 0x%x missing for" + "user=[%s] domain=[%s] " + "workstation=[%s]\n", + __func__, + (unsigned)sp->AvId, + ntlmssp_state->user, + ntlmssp_state->domain, + ntlmssp_state->client.netbios_name)); + return NT_STATUS_INVALID_PARAMETER; + } + + switch (cp->AvId) { +#define CASE_STRING(v) case Msv ## v: do { \ + int cmp; \ + if (sp->Value.v == NULL) { \ + return NT_STATUS_INTERNAL_ERROR; \ + } \ + if (cp->Value.v == NULL) { \ + DEBUG(1,("%s: invalid %s " \ + "got[%s] expect[%s] for " \ + "user=[%s] domain=[%s] workstation=[%s]\n", \ + __func__, #v, \ + cp->Value.v, \ + sp->Value.v, \ + ntlmssp_state->user, \ + ntlmssp_state->domain, \ + ntlmssp_state->client.netbios_name)); \ + return NT_STATUS_INVALID_PARAMETER; \ + } \ + cmp = strcmp(cp->Value.v, sp->Value.v); \ + if (cmp != 0) { \ + DEBUG(1,("%s: invalid %s " \ + "got[%s] expect[%s] for " \ + "user=[%s] domain=[%s] workstation=[%s]\n", \ + __func__, #v, \ + cp->Value.v, \ + sp->Value.v, \ + ntlmssp_state->user, \ + ntlmssp_state->domain, \ + ntlmssp_state->client.netbios_name)); \ + return NT_STATUS_INVALID_PARAMETER; \ + } \ +} while(0); break + CASE_STRING(AvNbComputerName); + CASE_STRING(AvNbDomainName); + CASE_STRING(AvDnsComputerName); + CASE_STRING(AvDnsDomainName); + CASE_STRING(AvDnsTreeName); + case MsvAvTimestamp: + if (cp->Value.AvTimestamp != sp->Value.AvTimestamp) { + struct timeval ct; + struct timeval st; + struct timeval_buf tmp1; + struct timeval_buf tmp2; + + nttime_to_timeval(&ct, + cp->Value.AvTimestamp); + nttime_to_timeval(&st, + sp->Value.AvTimestamp); + + DEBUG(1,("%s: invalid AvTimestamp " + "got[%s] expect[%s] for " + "user=[%s] domain=[%s] " + "workstation=[%s]\n", + __func__, + timeval_str_buf(&ct, false, + true, &tmp1), + timeval_str_buf(&st, false, + true, &tmp2), + ntlmssp_state->user, + ntlmssp_state->domain, + ntlmssp_state->client.netbios_name)); + return NT_STATUS_INVALID_PARAMETER; + } + break; + default: + /* + * This can't happen as we control + * ntlmssp_state->server.av_pair_list + */ + return NT_STATUS_INTERNAL_ERROR; + } + } + } + + nttime_to_timeval(&endtime, ntlmssp_state->server.challenge_endtime); + expired = timeval_expired(&endtime); + if (expired) { + struct timeval_buf tmp; + DEBUG(1,("%s: challenge invalid (expired %s) for " + "user=[%s] domain=[%s] workstation=[%s]\n", + __func__, + timeval_str_buf(&endtime, false, true, &tmp), + ntlmssp_state->user, ntlmssp_state->domain, + ntlmssp_state->client.netbios_name)); + return NT_STATUS_INVALID_PARAMETER; + } + /* NTLM2 uses a 'challenge' that is made of up both the server challenge, and a client challenge @@ -481,7 +752,8 @@ static NTSTATUS ntlmssp_server_check_password(struct gensec_security *gensec_sec static NTSTATUS ntlmssp_server_postauth(struct gensec_security *gensec_security, struct gensec_ntlmssp_context *gensec_ntlmssp, - struct ntlmssp_server_auth_state *state) + struct ntlmssp_server_auth_state *state, + DATA_BLOB request) { struct ntlmssp_state *ntlmssp_state = gensec_ntlmssp->ntlmssp_state; DATA_BLOB user_session_key = state->user_session_key; @@ -598,6 +870,55 @@ static NTSTATUS ntlmssp_server_postauth(struct gensec_security *gensec_security, talloc_steal(ntlmssp_state, session_key.data); } + if (ntlmssp_state->new_spnego) { + HMACMD5Context ctx; + uint8_t mic_buffer[NTLMSSP_MIC_SIZE] = { 0, }; + int cmp; + + hmac_md5_init_limK_to_64(ntlmssp_state->session_key.data, + ntlmssp_state->session_key.length, + &ctx); + + hmac_md5_update(ntlmssp_state->negotiate_blob.data, + ntlmssp_state->negotiate_blob.length, + &ctx); + hmac_md5_update(ntlmssp_state->challenge_blob.data, + ntlmssp_state->challenge_blob.length, + &ctx); + + /* checked were we set ntlmssp_state->new_spnego */ + SMB_ASSERT(request.length > + (NTLMSSP_MIC_OFFSET + NTLMSSP_MIC_SIZE)); + + hmac_md5_update(request.data, NTLMSSP_MIC_OFFSET, &ctx); + hmac_md5_update(mic_buffer, NTLMSSP_MIC_SIZE, &ctx); + hmac_md5_update(request.data + + (NTLMSSP_MIC_OFFSET + NTLMSSP_MIC_SIZE), + request.length - + (NTLMSSP_MIC_OFFSET + NTLMSSP_MIC_SIZE), + &ctx); + hmac_md5_final(mic_buffer, &ctx); + + cmp = memcmp(request.data + NTLMSSP_MIC_OFFSET, + mic_buffer, NTLMSSP_MIC_SIZE); + if (cmp != 0) { + DEBUG(1,("%s: invalid NTLMSSP_MIC for " + "user=[%s] domain=[%s] workstation=[%s]\n", + __func__, + ntlmssp_state->user, + ntlmssp_state->domain, + ntlmssp_state->client.netbios_name)); + dump_data(1, request.data + NTLMSSP_MIC_OFFSET, + NTLMSSP_MIC_SIZE); + dump_data(1, mic_buffer, + NTLMSSP_MIC_SIZE); + return NT_STATUS_INVALID_PARAMETER; + } + } + + data_blob_free(&ntlmssp_state->negotiate_blob); + data_blob_free(&ntlmssp_state->challenge_blob); + if (gensec_ntlmssp_have_feature(gensec_security, GENSEC_FEATURE_SIGN)) { nt_status = ntlmssp_sign_init(ntlmssp_state); } @@ -663,7 +984,7 @@ NTSTATUS gensec_ntlmssp_server_auth(struct gensec_security *gensec_security, ntlmssp_state->check_password, the ntlmssp_server_postpath can be done in a callback */ - nt_status = ntlmssp_server_postauth(gensec_security, gensec_ntlmssp, state); + nt_status = ntlmssp_server_postauth(gensec_security, gensec_ntlmssp, state, in); TALLOC_FREE(state); return nt_status; } -- cgit v1.2.1