Skip to content

Commit 875efe1

Browse files
committed
Pass 4: NTLMSSP hardening to match real Windows clients
Bring the NTLMSSP path closer to what a current Windows 10/11 client sends, addressing the IoC 19/23/27/28/29 cluster. - Always set NTLMSSP_NEGOTIATE_VERSION and NTLMSSP_NEGOTIATE_ALWAYS_SIGN on the Type 1 message; emit a default VERSION structure (10.0.<build>) when the caller does not override it. - Type 3 now defaults the same VERSION when none is supplied and uses the server-advertised MsvAvNbDomainName instead of the caller-supplied domain string. - exportedSessionKey: replace the printable ASCII random key with os.urandom(16). - AV pairs in NTLMv2: emit MsvAvFlags every time, set bit 0x2 when the challenge contains MsvAvTimestamp, and add MsvAvChannelBindings=Z(16) when no real channel binding is in use. - When the challenge advertises MsvAvTimestamp the client now zeroes the LM response (Z(24)) and computes a real MIC over the Negotiate || Challenge || Authenticate triplet, per [MS-NLMP] 3.1.5.1.2. - LDAP Sicily NTLM bind path: pass service='ldap' so MsvAvTargetName is stamped 'ldap/<host>' instead of 'cifs/<host>'.
1 parent b3eaa6e commit 875efe1

2 files changed

Lines changed: 62 additions & 16 deletions

File tree

impacket/ldap/ldap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def login(self, user='', password='', domain='', lmhash='', nthash='', authentic
387387
channel_binding_value = self.channel_binding_value
388388

389389
# NTLM Auth
390-
type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash, channel_binding_value=channel_binding_value)
390+
type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash, service='ldap', channel_binding_value=channel_binding_value)
391391
bindRequest['authentication']['sicilyResponse'] = type3.getData()
392392
response = self.sendReceive(bindRequest)[0]['protocolOp']
393393
elif authenticationChoice == 'sasl':

impacket/ntlm.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,15 @@ def ntlmssp_DES_encrypt(key, challenge):
589589

590590
# High level functions to use NTLMSSP
591591

592+
def _default_ntlm_version():
593+
"""Return a VERSION structure that looks like a current Windows 10/11 client."""
594+
v = VERSION()
595+
v['ProductMajorVersion'] = 10
596+
v['ProductMinorVersion'] = 0
597+
v['ProductBuild'] = random.choice([19041, 19042, 19044, 19045, 22000, 22621, 26100])
598+
v['NTLMRevisionCurrent'] = VERSION.NTLMSSP_REVISION_W2K3
599+
return v.getData()
600+
592601
def getNTLMSSPType1(workstation='', domain='', signingRequired = False, use_ntlmv2 = USE_NTLMv2, version = None):
593602
# Let's do some encoding checks before moving on. Kind of dirty, but found effective when dealing with
594603
# international characters.
@@ -608,16 +617,16 @@ def getNTLMSSPType1(workstation='', domain='', signingRequired = False, use_ntlm
608617
auth = NTLMAuthNegotiate()
609618
auth['flags']=0
610619
if signingRequired:
611-
auth['flags'] = NTLMSSP_NEGOTIATE_KEY_EXCH | NTLMSSP_NEGOTIATE_SIGN | NTLMSSP_NEGOTIATE_ALWAYS_SIGN | \
612-
NTLMSSP_NEGOTIATE_SEAL
620+
auth['flags'] = NTLMSSP_NEGOTIATE_KEY_EXCH | NTLMSSP_NEGOTIATE_SIGN | NTLMSSP_NEGOTIATE_SEAL
613621
if use_ntlmv2:
614622
auth['flags'] |= NTLMSSP_NEGOTIATE_TARGET_INFO
615623
auth['flags'] |= NTLMSSP_NEGOTIATE_NTLM | NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY | NTLMSSP_NEGOTIATE_UNICODE | \
616-
NTLMSSP_REQUEST_TARGET | NTLMSSP_NEGOTIATE_128 | NTLMSSP_NEGOTIATE_56
624+
NTLMSSP_REQUEST_TARGET | NTLMSSP_NEGOTIATE_128 | NTLMSSP_NEGOTIATE_56 | \
625+
NTLMSSP_NEGOTIATE_ALWAYS_SIGN | NTLMSSP_NEGOTIATE_VERSION
617626

618-
if version is not None:
619-
auth['flags'] |= NTLMSSP_NEGOTIATE_VERSION
620-
auth['os_version'] = version
627+
if version is None:
628+
version = _default_ntlm_version()
629+
auth['os_version'] = version
621630

622631
# We're not adding workstation / domain fields this time. Normally Windows clients don't add such information but,
623632
# we will save the workstation name to be used later.
@@ -702,7 +711,7 @@ def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash =
702711
if ntlmChallenge['flags'] & NTLMSSP_NEGOTIATE_KEY_EXCH:
703712
# not exactly what I call random tho :\
704713
# exportedSessionKey = this is the key we should use to sign
705-
exportedSessionKey = b("".join([random.choice(string.digits+string.ascii_letters) for _ in range(16)]))
714+
exportedSessionKey = os.urandom(16)
706715
#exportedSessionKey = "A"*16
707716
#print "keyExchangeKey %r" % keyExchangeKey
708717
# Let's generate the right session key based on the challenge flags
@@ -728,20 +737,47 @@ def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash =
728737
# [MS-NLMP] page 46
729738
exportedSessionKey = keyExchangeKey
730739

731-
ntlmChallengeResponse['flags'] = responseFlags
732-
ntlmChallengeResponse['domain_name'] = domain.encode('utf-16le')
740+
# If the server's CHALLENGE_MESSAGE included a Timestamp AV pair we are
741+
# expected to compute a MIC and zero out the LM response per [MS-NLMP]
742+
# 3.1.5.1.2. This is what every modern Windows client does.
743+
challenge_av = AV_PAIRS(serverName) if serverName else None
744+
have_timestamp = challenge_av is not None and challenge_av[NTLMSSP_AV_TIME] is not None
745+
compute_mic = have_timestamp and use_ntlmv2
746+
747+
# Use the NetBIOS domain advertised by the server when available, instead
748+
# of the caller-supplied value.
749+
if challenge_av is not None and challenge_av[NTLMSSP_AV_DOMAINNAME] is not None:
750+
wire_domain = challenge_av[NTLMSSP_AV_DOMAINNAME][1]
751+
else:
752+
wire_domain = domain.encode('utf-16le')
753+
754+
ntlmChallengeResponse['flags'] = responseFlags | NTLMSSP_NEGOTIATE_VERSION
755+
ntlmChallengeResponse['domain_name'] = wire_domain
733756
ntlmChallengeResponse['host_name'] = type1.getWorkstation().encode('utf-16le')
734-
if lmResponse == '':
757+
if compute_mic:
758+
# When timestamp is present the spec says LM response MUST be Z(24).
759+
ntlmChallengeResponse['lanman'] = b'\x00' * 24
760+
elif lmResponse == '':
735761
ntlmChallengeResponse['lanman'] = b'\x00'
736762
else:
737763
ntlmChallengeResponse['lanman'] = lmResponse
738764

739-
if version is not None:
740-
ntlmChallengeResponse['Version'] = version
765+
if version is None:
766+
version = _default_ntlm_version()
767+
ntlmChallengeResponse['Version'] = version
741768
ntlmChallengeResponse['ntlm'] = ntResponse
742-
if encryptedRandomSessionKey is not None:
769+
if encryptedRandomSessionKey is not None:
743770
ntlmChallengeResponse['session_key'] = encryptedRandomSessionKey
744771

772+
if compute_mic:
773+
# Reserve space for the MIC, serialize, then patch in the real value.
774+
ntlmChallengeResponse['MIC'] = b'\x00' * 16
775+
type1_data = type1.getData() if hasattr(type1, 'getData') else bytes(type1)
776+
type2_data = type2 if isinstance(type2, (bytes, bytearray)) else bytes(type2)
777+
type3_zero = ntlmChallengeResponse.getData()
778+
mic = hmac_md5(exportedSessionKey, type1_data + type2_data + type3_zero)
779+
ntlmChallengeResponse['MIC'] = mic
780+
745781
return ntlmChallengeResponse, exportedSessionKey
746782

747783

@@ -956,17 +992,27 @@ def computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, d
956992
# level
957993
if TEST_CASE is False:
958994
av_pairs[NTLMSSP_AV_TARGET_NAME] = f"{service}/".encode('utf-16le') + av_pairs[NTLMSSP_AV_DNS_HOSTNAME][1]
959-
if av_pairs[NTLMSSP_AV_TIME] is not None:
995+
timestamp_present = av_pairs[NTLMSSP_AV_TIME] is not None
996+
if timestamp_present:
960997
aTime = av_pairs[NTLMSSP_AV_TIME][1]
961998
else:
962999
aTime = struct.pack('<q', (116444736000000000 + calendar.timegm(time.gmtime()) * 10000000) )
9631000
av_pairs[NTLMSSP_AV_TIME] = aTime
1001+
# Always provide an MsvAvFlags entry. Bit 0x2 indicates the client
1002+
# provides a MIC, which is the case whenever the server sent a
1003+
# Timestamp AV pair in the challenge.
1004+
flags_value = 0x00000002 if timestamp_present else 0x00000000
1005+
av_pairs[NTLMSSP_AV_FLAGS] = struct.pack('<L', flags_value)
9641006
serverName = av_pairs.getData()
9651007
else:
9661008
aTime = b'\x00'*8
967-
1009+
9681010
if len(channel_binding_value) > 0:
9691011
av_pairs[NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value
1012+
elif TEST_CASE is False and av_pairs[NTLMSSP_AV_TIME] is not None:
1013+
# Spec: when MsvAvTimestamp is present and there is no real channel
1014+
# binding, set MsvAvChannelBindings to Z(16).
1015+
av_pairs[NTLMSSP_AV_CHANNEL_BINDINGS] = b'\x00' * 16
9701016

9711017
# Format according to:
9721018
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/aee311d6-21a7-4470-92a5-c4ecb022a87b

0 commit comments

Comments
 (0)