From 3232ec07e923c0358908912e4e369f5770e8b03f Mon Sep 17 00:00:00 2001 From: frank Date: Wed, 7 Feb 2024 23:22:04 +0100 Subject: [PATCH] Implemented LDAP channel binding as cleanly as I could, based on https://github.com/ly4k/ldap3. --- impacket/ldap/ldap.py | 25 ++++++++++++++++++++++++- impacket/ntlm.py | 34 ++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/impacket/ldap/ldap.py b/impacket/ldap/ldap.py index a82b35ddc8..8355699b1b 100644 --- a/impacket/ldap/ldap.py +++ b/impacket/ldap/ldap.py @@ -308,8 +308,31 @@ def login(self, user='', password='', domain='', lmhash='', nthash='', authentic # NTLM Challenge type2 = response['bindResponse']['matchedDN'] + # If TLS is used, setup channel binding + channel_binding_value = b'' + if self._SSL: + # From: https://github.com/ly4k/ldap3/commit/87f5760e5a68c2f91eac8ba375f4ea3928e2b9e0#diff-c782b790cfa0a948362bf47d72df8ddd6daac12e5757afd9d371d89385b27ef6R1383 + from hashlib import md5 + # Ugly but effective, to get the digest of the X509 DER in bytes + peer_cert_digest_str = self._socket.get_peer_certificate().digest('sha256').decode() + peer_cert_digest_bytes = bytes.fromhex(peer_cert_digest_str.replace(':', '')) + + channel_binding_struct = b'' + initiator_address = b'\x00'*8 + acceptor_address = b'\x00'*8 + + # https://datatracker.ietf.org/doc/html/rfc5929#section-4 + application_data_raw = b'tls-server-end-point:' + peer_cert_digest_bytes + len_application_data = len(application_data_raw).to_bytes(4, byteorder='little', signed = False) + application_data = len_application_data + application_data += application_data_raw + channel_binding_struct += initiator_address + channel_binding_struct += acceptor_address + channel_binding_struct += application_data + channel_binding_value = md5(channel_binding_struct).digest() + # NTLM Auth - type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash) + type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash, channel_binding_value=channel_binding_value) bindRequest['authentication']['sicilyResponse'] = type3.getData() response = self.sendReceive(bindRequest)[0]['protocolOp'] elif authenticationChoice == 'sasl': diff --git a/impacket/ntlm.py b/impacket/ntlm.py index f07cf93ce3..0f24929be4 100644 --- a/impacket/ntlm.py +++ b/impacket/ntlm.py @@ -37,10 +37,10 @@ def computeResponse(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash='', nthash='', - use_ntlmv2=USE_NTLMv2): + use_ntlmv2=USE_NTLMv2, channel_binding_value=''): if use_ntlmv2: return computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, - lmhash, nthash, use_ntlmv2=use_ntlmv2) + lmhash, nthash, use_ntlmv2=use_ntlmv2, channel_binding_value=channel_binding_value) else: return computeResponseNTLMv1(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash, nthash, use_ntlmv2=use_ntlmv2) @@ -594,7 +594,7 @@ def getNTLMSSPType1(workstation='', domain='', signingRequired = False, use_ntlm return auth -def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash = '', use_ntlmv2 = USE_NTLMv2): +def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash = '', use_ntlmv2 = USE_NTLMv2, channel_binding_value = ''): # Safety check in case somebody sent password = None.. That's not allowed. Setting it to '' and hope for the best. if password is None: @@ -633,7 +633,7 @@ def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash = ntResponse, lmResponse, sessionBaseKey = computeResponse(ntlmChallenge['flags'], ntlmChallenge['challenge'], clientChallenge, serverName, domain, user, password, - lmhash, nthash, use_ntlmv2) + lmhash, nthash, use_ntlmv2, channel_binding_value = channel_binding_value) # Let's check the return flags if (ntlmChallenge['flags'] & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) == 0: @@ -898,7 +898,7 @@ def LMOWFv2( user, password, domain, lmhash = ''): def computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash='', - nthash='', use_ntlmv2=USE_NTLMv2): + nthash='', use_ntlmv2=USE_NTLMv2, channel_binding_value=''): responseServerVersion = b'\x01' hiResponseServerVersion = b'\x01' @@ -919,9 +919,27 @@ def computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, d serverName = av_pairs.getData() else: aTime = b'\x00'*8 - - temp = responseServerVersion + hiResponseServerVersion + b'\x00' * 6 + aTime + clientChallenge + b'\x00' * 4 + \ - serverName + b'\x00' * 4 + + if channel_binding_value is None: + channel_binding_value = b'' + elif isinstance(channel_binding_value, str): + channel_binding_value = channel_binding_value.encode() + if len(channel_binding_value) > 0: + av_pairs[NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value + + # The following variable length AvPairs must be terminated like so + av_pairs[NTLMSSP_AV_EOL] = b'' + + # Format according to: + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/aee311d6-21a7-4470-92a5-c4ecb022a87b + temp = responseServerVersion # RespType 1 byte + temp += hiResponseServerVersion # HiRespType 1 byte + temp += b'\x00' * 2 # Reserved1 2 bytes + temp += b'\x00' * 4 # Reserved2 4 bytes + temp += aTime # TimeStamp 8 bytes + temp += clientChallenge # ChallengeFromClient 8 bytes + temp += b'\x00' * 4 # Reserved 4 bytes + temp += av_pairs.getData() # AvPairs variable ntProofStr = hmac_md5(responseKeyNT, serverChallenge + temp)