Skip to content

Commit

Permalink
LDAP Channel binding implementation from #1697 (#1844)
Browse files Browse the repository at this point in the history
* Implemented LDAP channel binding as cleanly as I could, based on https://github.com/ly4k/ldap3.

* Set channel binding to bytes value as requested in the review

* Fix test sessionBaseKey

* Fix test ntResponse

* Fix test encryptedSessionKey

* Fix test ntlmChallengeResponse

* Fix test ntlmChallengeResponse

* Removing leftover print statement

* Remove unnecessary AV_EOL, this is done by impackets struct anyway

---------

Co-authored-by: frank <[email protected]>
  • Loading branch information
NeffIsBack and frank authored Nov 25, 2024
1 parent 463693e commit ea27e8b
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 13 deletions.
25 changes: 24 additions & 1 deletion impacket/ldap/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,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
27 changes: 19 additions & 8 deletions impacket/ntlm.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
DEFAULT_LM_HASH = binascii.unhexlify('AAD3B435B51404EEAAD3B435B51404EE')

def computeResponse(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash='', nthash='',
use_ntlmv2=USE_NTLMv2):
use_ntlmv2=USE_NTLMv2, channel_binding_value=b''):
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 @@ -597,7 +597,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 = b''):

# 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 @@ -636,7 +636,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 @@ -909,7 +909,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=b''):

responseServerVersion = b'\x01'
hiResponseServerVersion = b'\x01'
Expand All @@ -930,9 +930,20 @@ 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 len(channel_binding_value) > 0:
av_pairs[NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value

# 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
8 changes: 4 additions & 4 deletions tests/SMB_RPC/test_ntlm.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ def test_ntlmv2(self):
ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponseNTLMv2(flags, self.serverChallenge,
self.clientChallenge, serverName, self.domain, self.user, self.password, '', '' )
hexdump(sessionBaseKey)
self.assertEqual(sessionBaseKey, bytearray(b'\x8d\xe4\x0c\xca\xdb\xc1\x4a\x82\xf1\x5c\xb0\xad\x0d\xe9\x5c\xa3'))
self.assertEqual(sessionBaseKey, bytearray(b'\xe0\x02\x92\x35\xf1\x18\x08\xe6\x12\xea\xa1\xac\xe6\x13\x78\x5f'))
print("\n")

print("4.2.4.2.1 LMv2 Response")
Expand All @@ -247,13 +247,13 @@ def test_ntlmv2(self):
print("\n")
print("4.2.4.2.2 NTLMv2 Response")
hexdump(ntResponse[:16])
self.assertEqual(ntResponse[:16], bytearray(b'\x68\xcd\x0a\xb8\x51\xe5\x1c\x96\xaa\xbc\x92\x7b\xeb\xef\x6a\x1c'))
self.assertEqual(ntResponse[:16], bytearray(b'\xb2\x32\x05\x0b\x98\xe5\xf4\xe3\x36\xbd\x18\x79\x21\xa2\x7b\xb2'))
print("\n")
print("4.2.4.2.3 Encrypted Session Key")
keyExchangeKey = ntlm.KXKEY(flags, sessionBaseKey, lmResponse, self.serverChallenge, self.password,'','')
encryptedSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey,self.randomSessionKey)
hexdump(encryptedSessionKey)
self.assertEqual(encryptedSessionKey, bytearray(b'\xC5\xDA\xD2\x54\x4F\xC9\x79\x90\x94\xCE\x1C\xE9\x0B\xC9\xD0\x3E'))
self.assertEqual(encryptedSessionKey, bytearray(b'\x18\x6c\xaf\xee\x66\x20\x16\x9d\xd9\x8c\x4d\x1a\x22\x56\x71\x4c'))
print("\n")

print("AUTHENTICATE MESSAGE")
Expand All @@ -266,7 +266,7 @@ def test_ntlmv2(self):
ntlmChallengeResponse['ntlm'] = ntResponse
ntlmChallengeResponse['session_key'] = encryptedSessionKey
hexdump(ntlmChallengeResponse.getData())
self.assertEqual(ntlmChallengeResponse.getData(), bytearray(b'NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00|\x00\x00\x00T\x00T\x00\x94\x00\x00\x00\x0c\x00\x0c\x00X\x00\x00\x00\x08\x00\x08\x00d\x00\x00\x00\x10\x00\x10\x00l\x00\x00\x00\x10\x00\x10\x00\xe8\x00\x00\x003\x82\x8a\xe2D\x00o\x00m\x00a\x00i\x00n\x00U\x00s\x00e\x00r\x00C\x00O\x00M\x00P\x00U\x00T\x00E\x00R\x00\x86\xc3P\x97\xac\x9c\xec\x10%TvJW\xcc\xcc\x19\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaah\xcd\n\xb8Q\xe5\x1c\x96\xaa\xbc\x92{\xeb\xefj\x1c\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x00\x02\x00\x0c\x00D\x00o\x00m\x00a\x00i\x00n\x00\x01\x00\x0c\x00S\x00e\x00r\x00v\x00e\x00r\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc5\xda\xd2TO\xc9y\x90\x94\xce\x1c\xe9\x0b\xc9\xd0>'))
self.assertEqual(ntlmChallengeResponse.getData(), bytearray(b'NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00|\x00\x00\x00P\x00P\x00\x94\x00\x00\x00\x0c\x00\x0c\x00X\x00\x00\x00\x08\x00\x08\x00d\x00\x00\x00\x10\x00\x10\x00l\x00\x00\x00\x10\x00\x10\x00\xe4\x00\x00\x003\x82\x8a\xe2D\x00o\x00m\x00a\x00i\x00n\x00U\x00s\x00e\x00r\x00C\x00O\x00M\x00P\x00U\x00T\x00E\x00R\x00\x86\xc3P\x97\xac\x9c\xec\x10%TvJW\xcc\xcc\x19\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xb22\x05\x0b\x98\xe5\xf4\xe36\xbd\x18y!\xa2{\xb2\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x00\x02\x00\x0c\x00D\x00o\x00m\x00a\x00i\x00n\x00\x01\x00\x0c\x00S\x00e\x00r\x00v\x00e\x00r\x00\x00\x00\x00\x00\x18l\xaf\xeef \x16\x9d\xd9\x8cM\x1a"VqL'))
print("\n")
print("4.2.4.4 GSS_WrapEx")
print("Plaintext")
Expand Down

0 comments on commit ea27e8b

Please sign in to comment.