Skip to content

Commit

Permalink
Implemented LDAP channel binding as cleanly as I could, based on http…
Browse files Browse the repository at this point in the history
  • Loading branch information
frank committed Feb 7, 2024
1 parent 97007e8 commit 3232ec0
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 9 deletions.
25 changes: 24 additions & 1 deletion impacket/ldap/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
34 changes: 26 additions & 8 deletions impacket/ntlm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'
Expand All @@ -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)

Expand Down

0 comments on commit 3232ec0

Please sign in to comment.