Skip to content

Overhaul ntlm.py, Spec-Aligned Hash Extraction and Additional Improvements #22

@StrongWind1

Description

@StrongWind1

Problem

The current ntlm.py has several spec-compliance gaps that affect hash capture quality, misclassify protocol variants, and leave crackable material on the table. This issue tracks all of the improvements needed to bring the module in line with [MS-NLMP].


The Five NTLM Response Types in a Type 3 Message

A single AUTHENTICATE_MESSAGE can contain up to five independently crackable response types. The current implementation extracts only one. Four of them map to existing hashcat modules:

# Type Crypto Construction Hashcat Mode Currently Captured?
1 NTLMv2 HMAC-MD5(NTOWFv2(password, user, domain), ServerChallenge ‖ Blob) 5600 / 27100 ⚠️ Partial (only this one)
2 LMv2 HMAC-MD5(NTOWFv2(password, user, domain), ServerChallenge ‖ ClientNonce) 5600 / 27100 ❌ Silently discarded
3 NTLMv1-ESS DESL(NTOWFv1(password), MD5(ServerChallenge ‖ ClientNonce)[0:8]) 5500 / 27000 ⚠️ May be misclassified
4 NTLMv1 DESL(NTOWFv1(password), ServerChallenge) 5500 / 27000 ⚠️ May be misclassified
5 LMv1 DESL(LMOWFv1(password), ServerChallenge) None (no module) ❌ Not applicable yet

Where per [MS-NLMP §6]:

  • NTOWFv1(password) = MD4(UTF-16LE(password)) — the NT hash
  • NTOWFv2(password, user, domain) = HMAC-MD5(NTOWFv1(password), UTF-16LE(Uppercase(user) + domain)) — the NTLMv2 session key
  • LMOWFv1(password) — DES-based, uppercase-only, ≤14 char limit — completely different from NTOWFv1
  • DESL(key, data) — splits a 16-byte key into three 7-byte DES keys, encrypts the 8-byte data with each, producing 3×8 = 24 bytes

LMv1 uses LMOWFv1() as its key source. Feeding an LM response to mode 5500 would apply NTOWFv1() instead — the wrong one-way function. A dedicated LMv1 module does not exist in hashcat yet (tracked in a companion issue). Real LM response bytes should still be carried in the NTLMv1 line's LM slot for hashcat's third-key DES optimization, but should not be emitted as a separate crackable entry until a module exists.


Issues in the Current Implementation

1. Version Detection Uses ESS Flag Instead of Payload Length

The current code uses the NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag to determine NTLMv1 vs NTLMv2:

# Current
if session_flags & ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY:
    version = "NTLMv1-SSP" if len(ntlm_data) == 24 else "NTLMv2-SSP"

Per [MS-NLMP §3.3.2], the ESS flag (0x00080000) governs NTLMv1 session security — it does not indicate NTLMv2. NTLMv2 is determined by the client's LmCompatibilityLevel registry setting, which is not visible on the wire. The only reliable discriminator is len(NtChallengeResponse) > 24:

  • NTLMv1: DESL() always produces exactly 24 bytes ([MS-NLMP §3.3.1])
  • NTLMv2: NTProofStr(16) + NTLMv2_CLIENT_CHALLENGE(≥28) is always > 24 bytes ([MS-NLMP §2.2.2.8])

Expected: Use payload length as the sole discriminator.

2. ESS Detection Relies Only on the Flag

The current code checks only the NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag to identify ESS captures. Per [MS-NLMP §3.3.1], when ESS is active the client sets LmChallengeResponse = ClientChallenge(8) ‖ Z(16) — this structural signature is the authoritative indicator. The flag can disagree due to negotiation quirks between client and server implementations.

Expected: Structural check (len(lm_resp) == 24 and lm_resp[8:] == Z(16)) should be authoritative. The flag should be supplementary, with a logged warning on disagreement.

3. Single Hash Extraction Per Type 3 Message

NTLM_AUTH_to_hashcat_format() returns a single (str, str). An NTLMv2 capture always contains both an NTLMv2 and an LMv2 response — two independently crackable hashes for mode 5600. The LMv2 companion hash is silently discarded.

Expected: Return list[tuple[str, str]] with all crackable hashes. NTLMv2 captures should emit both NTLMv2 and LMv2 entries. NTLMv1 captures should emit the NTLMv1/NTLMv1-SSP entry with the real LM response in the LM slot when valid.

4. No Extraction Filters

The current code does not filter any of the following cases, all of which produce uncrackable or misleading capture database entries:

Filter Needed What It Catches Spec Reference
Dummy LM filtering DESL(Z(16), Challenge) and DESL(LMOWFv1(""), Challenge) — deterministic values with no crackable password material §3.3.1
Level 2 dedup LmChallengeResponse == NtChallengeResponse — at LmCompatibilityLevel 2 the client copies NT response into both fields; the LM slot is not an LM hash §3.3.1
Anonymous bypass NTLMSSP_NEGOTIATE_ANONYMOUS flag set, or empty UserName + empty NtChallengeResponse + empty/Z(1) LmChallengeResponse §3.2.5.1.2
Null LMv2 skip All-zero LmChallengeResponse when MsvAvTimestamp is present in server's AV_PAIRS — client sets LmChallengeResponse to Z(24) §3.1.5.1.2

5. NTLMSSP_AV_TIME Causes LMv2 to Be Nulled

Per [MS-NLMP §3.1.5.1.2], when MsvAvTimestamp (NTLMSSP_AV_TIME) is present in the CHALLENGE_MESSAGE's AV_PAIRS, the client sets LmChallengeResponse to Z(24) — nulling the LMv2 hash entirely. This means the current implementation, which always populates NTLMSSP_AV_TIME in TargetInfoFields, guarantees that LMv2 hashes are never captured from any modern Windows client.

On Windows 7 / Server 2008 R2 and newer, this behavior is unconditional unless the registry key HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\LSA\SuppressExtendedProtection is set to 1. See Microsoft documentation.

Expected: Remove NTLMSSP_AV_TIME from the AV_PAIRS populated in NTLM_AUTH_CreateChallenge(). This allows LMv2 responses to be captured as a companion hash alongside NTLMv2 responses. The timestamp is not required for capture server operation — Dementor does not verify responses or compute session keys.

6. Missing SEAL and ALWAYS_SIGN Flag Echoing

The current NTLM_AUTH_CreateChallenge() echoes NTLMSSP_NEGOTIATE_SIGN but misses NTLMSSP_NEGOTIATE_SEAL and NTLMSSP_NEGOTIATE_ALWAYS_SIGN. Some clients drop the connection when these aren't echoed, losing the capture before the AUTHENTICATE_MESSAGE is sent. Per [MS-NLMP §3.2.5.1.1], the server should echo client-requested capability flags.

Expected: Echo the full set: UNICODE, OEM, 56, 128, KEY_EXCH, SIGN, SEAL, ALWAYS_SIGN.

7. No ESS / LM_KEY Mutual Exclusivity

Per [MS-NLMP §2.2.2.5 flag P], NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY and NTLMSSP_NEGOTIATE_LM_KEY are mutually exclusive. The current code does not clear LM_KEY when ESS is active.

Expected: if ESS set: clear LM_KEY.

8. No disable_ntlmv2 Option

There is no way to omit TargetInfoFields from the CHALLENGE_MESSAGE. Without this option, there is no mechanism to force NTLMv1 captures from level 0-2 clients. Per [MS-NLMP §2.2.2.7], the NTLMv2 blob requires AV_PAIRS from TargetInfoFields — if they're absent, the client cannot construct the NTLMv2 response and falls back to NTLMv1 (level 0-2) or fails auth (level 3+).

Expected: A disable_ntlmv2 boolean parameter on NTLM_AUTH_CreateChallenge() that clears NTLMSSP_NEGOTIATE_TARGET_INFO, sets TargetInfoFields_len/max_len to 0, and omits AV_PAIRS.

9. Configuration Attribute Naming

Minor issues with the current config surface:

  • ntlm_challange — typo (should be ntlm_challenge)
  • ntlm_ess — enable-style boolean (True = on) with ambiguous polarity. A disable-style attribute (ntlm_disable_ess, default False) makes the "setting this to True is a deliberate downgrade" semantics explicit
  • No challenge format prefixes — bare values are ambiguous when an 8-character ASCII string happens to be valid hex

Expected:

Old Attribute New Attribute TOML Key Default Change
ntlm_challange ntlm_challenge NTLM.Challenge Random 8 bytes Typo fix + hex:/ascii: prefix support
ntlm_ess (enable bool) ntlm_disable_ess NTLM.DisableExtendedSessionSecurity False Renamed + inverted polarity
(none) ntlm_disable_ntlmv2 NTLM.DisableNTLMv2 False New: omit TargetInfoFields to block NTLMv2

Expected Output Formats

Once resolved, NTLM_AUTH_to_hashcat_formats() should return the following for each protocol path — all directly consumable by hashcat modes 5500/5600 (and their 27000/27100 NT-hash variants) with no post-processing:

Authentication Type Emitted Labels Hashcat Modes Format String
NTLMv2 "NTLMv2" + "LMv2" 5600, 27100 User::Domain:SrvChal:NTProof:Blob
NTLMv1 with ESS "NTLMv1-SSP" 5500, 27000 User::Domain:ClientNonce‖Z(16):NtResp:SrvChal
NTLMv1 pure "NTLMv1" 5500, 27000 User::Domain:LmResp:NtResp:SrvChal

The LM slot in NTLMv1 lines should be either the real LM response (for hashcat's DES optimization), empty (when dummy-filtered or deduplicated), or ClientNonce ‖ Z(16) (for ESS, which hashcat detects automatically).


References

  • [MS-NLMP] — NT LAN Manager (NTLM) Authentication Protocol, v20240423
  • hashcat mode 5500 — module_05500.c (NetNTLMv1 / NetNTLMv1+ESS)
  • hashcat mode 5600 — module_05600.c (NetNTLMv2)
  • hashcat mode 27000 — module_27000.c (NetNTLMv1 / NetNTLMv1+ESS, NT hash wordlist)
  • hashcat mode 27100 — module_27100.c (NetNTLMv2, NT hash wordlist)
  • Microsoft — Authentication fails when using NTLM with non-Windows Kerberos servers
  • impacket ntlm.py — NTLM flag constants and DES primitives

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions