From cdfa26f68a449963f97b034167f555b432c3f8e1 Mon Sep 17 00:00:00 2001 From: Dustin Heywood Date: Sun, 2 Nov 2025 20:05:50 -0700 Subject: [PATCH 1/4] Refactor NTLM parsing to use regex --- src/ntlmv1.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ntlmv1.py b/src/ntlmv1.py index 586e4c3..2b50aac 100755 --- a/src/ntlmv1.py +++ b/src/ntlmv1.py @@ -13,6 +13,7 @@ import binascii import json from Crypto.Cipher import DES +import re def f_ntlm_des(key_7_bytes_hex): @@ -215,14 +216,9 @@ def parse_mschapv2(mschapv2_input, key1=None, key2=None, json_mode=False): ntresp = None source = None - if s.startswith("$MSCHAPv2$") or s.startswith("$NETNTLM$") or s.startswith("$NETNTLMv1$"): - parts = s.split("$") - if len(parts) >= 4: - chal = parts[2] - ntresp = parts[3] - source = parts[1] - else: - raise ValueError("Invalid $MSCHAPv2$/NETNTLM format") + m = re.search(r'\$(MSCHAPv2|NETNTLM|NETNTLMv1)\$([0-9A-Fa-f]{16})\$([0-9A-Fa-f]{48})', s) + if m: + source, chal, ntresp = m.group(1), m.group(2), m.group(3) elif ":" in s and "$" not in s: fields = s.split(":") @@ -422,7 +418,6 @@ def main(): args.mschapv2, key1=args.key1, key2=args.key2, - show_pt3=True, json_mode=args.json ) except Exception as e: From b0f496104cf65e923a5925e176c7b6fc0838b118 Mon Sep 17 00:00:00 2001 From: evilmog Date: Fri, 7 Nov 2025 12:30:00 -0700 Subject: [PATCH 2/4] added password to key derivation --- src/ntlmv1.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/ntlmv1.py b/src/ntlmv1.py index 2b50aac..d4bd5f5 100755 --- a/src/ntlmv1.py +++ b/src/ntlmv1.py @@ -13,9 +13,28 @@ import binascii import json from Crypto.Cipher import DES +from Crypto.Hash import MD4 import re +def generate_ntlm_hash(password): + """ + Generates the NTLM hash (MD4) for a given password. + The password is first encoded in UTF-16LE. + """ + # Encode the password in UTF-16LE + password_bytes = password.encode('utf-16le') + + # Create an MD4 hash object + md4_hash = MD4.new() + + # Update the hash object with the encoded password + md4_hash.update(password_bytes) + + # Return the hexadecimal digest of the hash + return md4_hash.hexdigest() + + def f_ntlm_des(key_7_bytes_hex): key_bytes = bytes.fromhex(key_7_bytes_hex) key = [] @@ -313,6 +332,7 @@ def main(): parser.add_argument("--nthash", help="32-char hex NTLM hash to compute DES keys and hashcat candidates") parser.add_argument("--mschapv2", help="MSCHAPv2 line in $MSCHAPv2$CHALLENGE$NTRESPONSE format") parser.add_argument("--to-mschapv2", action="store_true", help="Convert NTLMv1 hash to $MSCHAPv2$ format") + parser.add_argument("--password", help="Convert password into des keys for --key1 and --key 2") args = parser.parse_args() @@ -322,6 +342,19 @@ def main(): output = {} + # if password is given, and key1/key2 not explicitly set, derive them automatically + + if args.password and (not args.key1 or not args.key2): + try: + nthash = generate_ntlm_hash(args.password) + if not args.nthash: + args.nthash = nthash + k1, k2, k3 = ntlm_to_des_keys(nthash) + args.key1 = k1 + args.key2 = k2 + except Exception as e: + print(f"[!] Failed to derive DES keys from NTLM hash: {e}") + # If NTLM is given and key1/key2 not explicitly set, derive them automatically if args.nthash and (not args.key1 or not args.key2): try: From 3da26ed0f8927f9e6e4f03d73c3a727a4658561a Mon Sep 17 00:00:00 2001 From: evilmog Date: Fri, 7 Nov 2025 13:04:56 -0700 Subject: [PATCH 3/4] fixed key derivation --- src/ntlmv1.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/ntlmv1.py b/src/ntlmv1.py index d4bd5f5..998c74e 100755 --- a/src/ntlmv1.py +++ b/src/ntlmv1.py @@ -95,6 +95,8 @@ def decode_and_validate_99(enc_99): "ct2": raw[16:24].hex(), "pt3": raw[24:26].hex(), "ct3": None, + "k1": None, + "k2": None, "pt1": None, "pt2": None, } @@ -192,6 +194,8 @@ def parse_ntlmv1(ntlmv1_hash, key1=None, key2=None, show_pt3=True, json_mode=Fal "ct1": ct1, "ct2": ct2, "ct3": ct3, + "k1" : None, + "k2" : None, "pt1": None, "pt2": None, "pt3": None, @@ -202,12 +206,14 @@ def parse_ntlmv1(ntlmv1_hash, key1=None, key2=None, show_pt3=True, json_mode=Fal encrypted1 = des_encrypt_block(key1, challenge) if encrypted1 and encrypted1.lower() == ct1.lower(): pt1 = des_to_ntlm_slice(key1) + data["k1"] = key1 data["pt1"] = pt1 if key2 and len(key2) == 16: encrypted2 = des_encrypt_block(key2, challenge) if encrypted2 and encrypted2.lower() == ct2.lower(): pt2 = des_to_ntlm_slice(key2) + data["k2"] = key2 data["pt2"] = pt2 pt3 = recover_key_from_ct3(data["ct3"], data["client_challenge"], data["lmresp"]) @@ -218,7 +224,7 @@ def parse_ntlmv1(ntlmv1_hash, key1=None, key2=None, show_pt3=True, json_mode=Fal if not json_mode: print("\n[+] NTLMv1 Parsed:") - for field in ["username", "domain", "challenge", "ct1", "ct2", "ct3", "pt1", "pt2", "pt3", "ntlm"]: + for field in ["username", "domain", "challenge", "ct1", "ct2", "ct3", "k1", "k2" ,"pt1", "pt2", "pt3", "ntlm"]: print(f"{field.upper():>12}: {data.get(field)}") return data @@ -260,6 +266,8 @@ def parse_mschapv2(mschapv2_input, key1=None, key2=None, json_mode=False): "ct1": ct1, "ct2": ct2, "ct3": ct3, + "k1" : None, + "k2" : None, "pt1": None, "pt2": None, "pt3": None, @@ -270,11 +278,13 @@ def parse_mschapv2(mschapv2_input, key1=None, key2=None, json_mode=False): encrypted1 = des_encrypt_block(key1, chal) if encrypted1 and encrypted1.lower() == ct1.lower(): data["pt1"] = des_to_ntlm_slice(key1) + data["k1"] = key1 if key2 and len(key2) == 16: encrypted2 = des_encrypt_block(key2, chal) if encrypted2 and encrypted2.lower() == ct2.lower(): data["pt2"] = des_to_ntlm_slice(key2) + data["k2"] = key2 data["pt3"] = recover_key_from_ct3(data["ct3"], chal) @@ -283,7 +293,7 @@ def parse_mschapv2(mschapv2_input, key1=None, key2=None, json_mode=False): if not json_mode: print("\n[+] MSCHAPv2 Parsed:") - for field in ["challenge", "ct1", "ct2", "ct3", "pt1", "pt2", "pt3", "ntlm"]: + for field in ["challenge", "ct1", "ct2", "ct3", "k1", "k2", "pt1", "pt2", "pt3", "ntlm"]: print(f"{field.upper():>12}: {data.get(field)}") return data @@ -366,19 +376,19 @@ def main(): except Exception as e: print(f"[!] Failed to derive DES keys from NTLM hash: {e}") - - if args.hash_99: data_99 = decode_and_validate_99(args.hash_99) if args.key1: encrypted1 = des_encrypt_block(args.key1, data_99["challenge"]) if encrypted1 and encrypted1.lower() == data_99["ct1"].lower(): + data_99["k1"] = args.key1 data_99["pt1"] = des_to_ntlm_slice(args.key1) if args.key2: encrypted2 = des_encrypt_block(args.key2, data_99["challenge"]) if encrypted2 and encrypted2.lower() == data_99["ct2"].lower(): + data_99["k2"] = args.key2 data_99["pt2"] = des_to_ntlm_slice(args.key2) # Optional: compute full NTLM hash if all parts are present @@ -389,7 +399,7 @@ def main(): if not args.json: print("\n[+] $99$ Parsed:") - for field in ["client_challenge", "ct1", "ct2", "ct3", "pt1", "pt2", "pt3", "ntlm"]: + for field in ["client_challenge", "ct1", "ct2", "ct3", "k1", "k2", "pt1", "pt2", "pt3", "ntlm"]: print(f"{field.upper():>20}: {data_99.get(field)}") if args.ntlmv1: @@ -449,8 +459,8 @@ def main(): try: output["mschapv2"] = parse_mschapv2( args.mschapv2, - key1=args.key1, - key2=args.key2, + args.key1, + args.key2, json_mode=args.json ) except Exception as e: From a5ebe0aba9c82c9e9e030520a9345e421f663198 Mon Sep 17 00:00:00 2001 From: evilmog Date: Fri, 7 Nov 2025 13:32:27 -0700 Subject: [PATCH 4/4] fixed key derivation display --- src/ntlmv1.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ntlmv1.py b/src/ntlmv1.py index 998c74e..c40efdf 100755 --- a/src/ntlmv1.py +++ b/src/ntlmv1.py @@ -206,14 +206,12 @@ def parse_ntlmv1(ntlmv1_hash, key1=None, key2=None, show_pt3=True, json_mode=Fal encrypted1 = des_encrypt_block(key1, challenge) if encrypted1 and encrypted1.lower() == ct1.lower(): pt1 = des_to_ntlm_slice(key1) - data["k1"] = key1 data["pt1"] = pt1 if key2 and len(key2) == 16: encrypted2 = des_encrypt_block(key2, challenge) if encrypted2 and encrypted2.lower() == ct2.lower(): pt2 = des_to_ntlm_slice(key2) - data["k2"] = key2 data["pt2"] = pt2 pt3 = recover_key_from_ct3(data["ct3"], data["client_challenge"], data["lmresp"]) @@ -224,7 +222,7 @@ def parse_ntlmv1(ntlmv1_hash, key1=None, key2=None, show_pt3=True, json_mode=Fal if not json_mode: print("\n[+] NTLMv1 Parsed:") - for field in ["username", "domain", "challenge", "ct1", "ct2", "ct3", "k1", "k2" ,"pt1", "pt2", "pt3", "ntlm"]: + for field in ["username", "domain", "challenge", "ct1", "ct2", "ct3" ,"pt1", "pt2", "pt3", "ntlm"]: print(f"{field.upper():>12}: {data.get(field)}") return data @@ -278,13 +276,11 @@ def parse_mschapv2(mschapv2_input, key1=None, key2=None, json_mode=False): encrypted1 = des_encrypt_block(key1, chal) if encrypted1 and encrypted1.lower() == ct1.lower(): data["pt1"] = des_to_ntlm_slice(key1) - data["k1"] = key1 if key2 and len(key2) == 16: encrypted2 = des_encrypt_block(key2, chal) if encrypted2 and encrypted2.lower() == ct2.lower(): data["pt2"] = des_to_ntlm_slice(key2) - data["k2"] = key2 data["pt3"] = recover_key_from_ct3(data["ct3"], chal) @@ -293,7 +289,7 @@ def parse_mschapv2(mschapv2_input, key1=None, key2=None, json_mode=False): if not json_mode: print("\n[+] MSCHAPv2 Parsed:") - for field in ["challenge", "ct1", "ct2", "ct3", "k1", "k2", "pt1", "pt2", "pt3", "ntlm"]: + for field in ["challenge", "ct1", "ct2", "ct3", "pt1", "pt2", "pt3", "ntlm"]: print(f"{field.upper():>12}: {data.get(field)}") return data