-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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 | |
| 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 | |
| 4 | NTLMv1 | DESL(NTOWFv1(password), ServerChallenge) |
5500 / 27000 | |
| 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 hashNTOWFv2(password, user, domain) = HMAC-MD5(NTOWFv1(password), UTF-16LE(Uppercase(user) + domain))— the NTLMv2 session keyLMOWFv1(password)— DES-based, uppercase-only, ≤14 char limit — completely different from NTOWFv1DESL(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 bentlm_challenge)ntlm_ess— enable-style boolean (True= on) with ambiguous polarity. A disable-style attribute (ntlm_disable_ess, defaultFalse) 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