-
Notifications
You must be signed in to change notification settings - Fork 102
Expand file tree
/
Copy pathntlmv1-nextgen.py
More file actions
executable file
·471 lines (389 loc) · 16.3 KB
/
ntlmv1-nextgen.py
File metadata and controls
executable file
·471 lines (389 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# /*
# * Author.......: Dustin Heywood <dustin.heywood@gmail.com> (EvilMog)
# * Used C code stolen from .......: Jens Steube <jens.steube@gmail.com>
# * Thus this code is under the same license
# * License.....: MIT
# *
# * Most of the C code taken from hashcat use for the python port
# */
import argparse
import base64
import hashlib
import binascii
import json
from Crypto.Cipher import DES
from Crypto.Hash import MD4
import re
def ntlm_hex_to_deskeys(ntlm_hex: str) -> tuple[str, str]:
"""
NTLM (32 hex chars) -> two DES keys (16 hex chars each).
Parity is FORCED to 1 and appended as the LSB of each output byte
(matches your original behavior).
"""
ntlm_hex = ntlm_hex.strip().lower()
PARITY = 1 # forced
def expand(part_hex: str) -> str:
b = bytes.fromhex(part_hex) # 7 bytes (56 bits)
out = bytearray(8)
for j in range(8):
v = 0
for k in range(7):
i = 7 * j + k
src_byte = i // 8
src_bit = 7 - (i % 8) # MSB-first
v = (v << 1) | ((b[src_byte] >> src_bit) & 1)
out[j] = (v << 1) | PARITY # parity as LSB
return out.hex()
return expand(ntlm_hex[:14]), expand(ntlm_hex[14:28])
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 = []
key.append(key_bytes[0])
key.append((key_bytes[0] << 7 | key_bytes[1] >> 1) & 0xFF)
key.append((key_bytes[1] << 6 | key_bytes[2] >> 2) & 0xFF)
key.append((key_bytes[2] << 5 | key_bytes[3] >> 3) & 0xFF)
key.append((key_bytes[3] << 4 | key_bytes[4] >> 4) & 0xFF)
key.append((key_bytes[4] << 3 | key_bytes[5] >> 5) & 0xFF)
key.append((key_bytes[5] << 2 | key_bytes[6] >> 6) & 0xFF)
key.append((key_bytes[6] << 1) & 0xFF)
for i in range(8):
# Ensure odd parity for each byte
b = key[i]
parity = 0
for bit in range(7):
parity += (b >> bit) & 1
if parity % 2 == 0:
key[i] |= 1 # set LSB to 1
else:
key[i] &= 0xFE # set LSB to 0
return ''.join(f'{b:02x}' for b in key)
def ntlm_to_des_keys(ntlm_hash):
if len(ntlm_hash) != 32:
raise ValueError("NTLM hash must be 32 hex characters")
k1_hex = f_ntlm_des(ntlm_hash[0:14])
k2_hex = f_ntlm_des(ntlm_hash[14:28])
k3_hex = f_ntlm_des(ntlm_hash[28:32] + "000000000000") # pad to 14 chars
return k1_hex, k2_hex, k3_hex
def des_to_ntlm_slice(deskey_hex):
deskey = bytes.fromhex(deskey_hex)
bits = ''.join([f"{byte:08b}" for byte in deskey])
stripped = ''.join([bits[i:i+7] for i in range(0, 64, 8)])
ntlm_bytes = int(stripped, 2).to_bytes(7, 'big')
return ntlm_bytes.hex()
def decode_and_validate_99(enc_99):
if not enc_99.startswith("$99$"):
raise ValueError("Invalid $99$ prefix")
b64_data = enc_99[4:].strip().rstrip("=")
b64_data += "=" * ((4 - len(b64_data) % 4) % 4)
raw = base64.b64decode(b64_data)
if len(raw) != 26:
raise ValueError(f"Expected 26 bytes, got {len(raw)}")
return {
"source": "$99$",
"client_challenge": raw[0:8].hex(),
"server_challenge": raw[0:8].hex(),
"challenge": raw[0:8].hex(),
"ct1": raw[8:16].hex(),
"ct2": raw[16:24].hex(),
"pt3": raw[24:26].hex(),
}
def des_encrypt_block(key8_hex, challenge_hex):
if len(key8_hex) != 16 or len(challenge_hex) != 16:
return None
key_bytes = bytes.fromhex(key8_hex)
challenge_bytes = bytes.fromhex(challenge_hex)
cipher = DES.new(key_bytes, DES.MODE_ECB)
return cipher.encrypt(challenge_bytes).hex()
def recover_key_from_ct3(ct3_hex, challenge_hex, ess_hex=None):
# Convert hex inputs to bytes
ct3_bytes = bytes.fromhex(ct3_hex)
challenge_bytes = bytes.fromhex(challenge_hex)
if len(ct3_bytes) != 8 or len(challenge_bytes) != 8:
raise ValueError("ct3 and challenge must be 8 bytes (16 hex chars) each")
# Convert bytes to integer representation
ct3_val = int.from_bytes(ct3_bytes, 'big')
challenge_val = int.from_bytes(challenge_bytes, 'big')
# Handle ESS case using fast MD5 hash
if ess_hex:
ess_bytes = bytes.fromhex(ess_hex)
if len(ess_bytes) != 24:
raise ValueError("ESS must be 24 bytes (48 hex chars)")
if ess_bytes[8:] == b'\x00' * 16:
challenge_bytes = hashlib.md5(challenge_bytes + ess_bytes[:8]).digest()[:8]
challenge_val = int.from_bytes(challenge_bytes, 'big')
# **Optimized DES brute-force loop**
found_key = None
for i in range(0x10000): # 16-bit key space
# **Optimized 7-byte to 8-byte DES key transformation**
nthash_bytes = [
i & 0xFF,
(i >> 8) & 0xFF,
0, 0, 0, 0, 0
]
key_bytes = bytes([
nthash_bytes[0] | 1,
((nthash_bytes[0] << 7) | (nthash_bytes[1] >> 1)) & 0xFF | 1,
((nthash_bytes[1] << 6) | (nthash_bytes[2] >> 2)) & 0xFF | 1,
((nthash_bytes[2] << 5) | (nthash_bytes[3] >> 3)) & 0xFF | 1,
((nthash_bytes[3] << 4) | (nthash_bytes[4] >> 4)) & 0xFF | 1,
((nthash_bytes[4] << 3) | (nthash_bytes[5] >> 5)) & 0xFF | 1,
((nthash_bytes[5] << 2) | (nthash_bytes[6] >> 6)) & 0xFF | 1,
((nthash_bytes[6] << 1)) & 0xFF | 1
])
# **Use PyCryptodome for fast DES encryption**
cipher = DES.new(key_bytes, DES.MODE_ECB)
encrypted = cipher.encrypt(challenge_bytes)
# **Fast integer comparison instead of byte-by-byte check**
if int.from_bytes(encrypted, 'big') == ct3_val:
found_key = i
break
if found_key is None:
return None # Key not found
# **Return key in correct format (low-order byte first, as in C output)**
return f"{found_key & 0xFF:02x}{(found_key >> 8) & 0xFF:02x}"
def parse_ntlmv1(ntlmv1_hash, key1=None, key2=None, show_pt3=True, json_mode=False):
fields = ntlmv1_hash.strip().split(':')
if len(fields) < 6:
raise ValueError("Invalid NTLMv1 format")
user, domain, lmresp, ntresp, challenge = fields[0], fields[2], fields[3], fields[4], fields[5]
ct1, ct2, ct3 = ntresp[0:16], ntresp[16:32], ntresp[32:48]
ess = None
if lmresp[20:] == "0000000000000000000000000000":
ess = lmresp
m = hashlib.md5()
m.update(binascii.unhexlify(challenge + lmresp[:16]))
challenge = m.digest()[:8].hex()
data = {
"username": user.upper(),
"domain": domain.upper(),
"client_challenge": fields[5].upper(),
"server_challenge": lmresp[:16].upper(),
"challenge": challenge.upper(),
"lmresp": lmresp.upper(),
"ntresp": ntresp.upper(),
"ct1": ct1.upper(),
"ct2": ct2.upper(),
"ct3": ct3.upper()
}
else:
challenge = fields[5].upper()
data = {
"username": user.upper(),
"domain": domain.upper(),
"challenge": challenge.upper(),
"lmresp": lmresp.upper(),
"ntresp": ntresp.upper(),
"ct1": ct1.upper(),
"ct2": ct2.upper(),
"ct3": ct3.upper()
}
if key1 and len(key1) == 16:
encrypted1 = des_encrypt_block(key1, challenge)
if encrypted1 and encrypted1.lower() == ct1.lower():
pt1 = des_to_ntlm_slice(key1)
data["pt1"] = pt1.upper()
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["pt2"] = pt2.upper()
pt3 = recover_key_from_ct3(data["ct3"], data["challenge"], data["lmresp"])
data["pt3"] = pt3.upper()
if data.get("pt1") and data.get("pt2") and data.get("pt3"):
data["ntlm"] = data.get("pt1") + data.get("pt2") + data.get("pt3")
if key1 and len(key1) == 16:
data["key1"] = key1.upper()
if key2 and len(key2) == 16:
data["key2"] = key2.upper()
if not json_mode:
print("\n[+] NTLMv1 Parsed:")
width = max(len(k) for k in data)
for k, v in data.items():
print(f"{k.upper():>{width}}: {v}")
return data
def parse_mschapv2(mschapv2_input, key1=None, key2=None, json_mode=False):
"""
Accepts:
- $NETNTLM$... or $NETNTLMv1$... (treated the same)
- Colon form: <user>::<domain>:<auth>:<peer>:<ntresp> → last two are challenge + NT response
"""
# Look I vibe coded this section, I need to fix it, this section is bad and I'm sorry
# - EvilMog
# use the JTR version out of EAP-MANA, you basically just need to parse enough info to get
# the challenge, and the ntresponse, the "source" is basically a marker to determine how
# this was derived, which parser
s = mschapv2_input.strip()
chal = None
ntresp = None
source = None
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(":")
if len(fields) >= 2:
chal = fields[-2]
ntresp = fields[-1]
source = "colon"
else:
raise ValueError("Invalid colon format")
else:
raise ValueError("Unrecognized MSCHAPv2 format")
ct1, ct2, ct3 = ntresp[0:16], ntresp[16:32], ntresp[32:48]
data = {
"challenge": chal,
"ct1": ct1,
"ct2": ct2,
"ct3": ct3
}
if key1 and len(key1) == 16:
encrypted1 = des_encrypt_block(key1, chal)
if encrypted1 and encrypted1.lower() == ct1.lower():
data["pt1"] = des_to_ntlm_slice(key1).upper()
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).upper()
data["pt3"] = recover_key_from_ct3(data["ct3"], chal).upper()
if data.get("pt1") and data.get("pt2") and data.get("pt3"):
data["ntlm"] = data["pt1"] + data["pt2"] + data["pt3"]
if key1 and len(key1) == 16:
data["key1"] = key1.upper()
if key2 and len(key2) == 16:
data["key2"] = key2.upper()
if not json_mode:
print("\n[+] MSCHAPv2 Parsed:")
width = max(len(k) for k in data)
for k, v in data.items():
print(f"{k.upper():>{width}}: {v}")
return data
def ntlmv1_to_99(parsed):
try:
challenge = bytes.fromhex(parsed["challenge"])
ct1 = bytes.fromhex(parsed["ct1"])
ct2 = bytes.fromhex(parsed["ct2"])
pt3 = bytes.fromhex(parsed["pt3"]) # pt3 is already recovered via parse_ntlmv1()
raw = challenge + ct1 + ct2 + pt3
b64 = base64.b64encode(raw).decode().rstrip("=")
return f"$99${b64}"
except Exception as e:
print(f"[-] Failed to convert to $99$: {e}")
return None
def main():
parser = argparse.ArgumentParser(description="NTLMv1/$99$ parser with correct DES key handling and CT3 recovery.")
parser.add_argument("--ntlmv1", help="NTLMv1 hash (Responder format)")
parser.add_argument("--99", dest="hash_99", help="$99$ style base64 blob")
parser.add_argument("--key1", help="16-char DES key hex for CT1")
parser.add_argument("--key2", help="16-char DES key hex for CT2")
parser.add_argument("--json", action="store_true", help="Output JSON only")
parser.add_argument("--hashcat", action="store_true", help="Generate hashcat format strings for ct1/ct2")
parser.add_argument("--nthash", help="32-char hex NTLM hash to compute DES keys and hashcat candidates")
parser.add_argument("--mschapv2", help="jtr format MSCHAPv2 Hash")
parser.add_argument("--password", help="Convert password into des keys for --key1 and --key 2")
args = parser.parse_args()
if len(vars(args)) == 0 or all(v is None or v is False for v in vars(args).values()):
parser.print_help()
return
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 = ntlm_hex_to_deskeys(args.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:
k1, k2 = ntlm_hex_to_deskeys(args.nthash)
if not args.key1:
args.key1 = k1
if not args.key2:
args.key2 = k2
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
if data_99.get("pt1") and data_99.get("pt2") and data_99.get("pt3"):
data_99["ntlm"] = data_99["pt1"] + data_99["pt2"] + data_99["pt3"]
output["$99$"] = data_99
if not args.json:
print("\n[+] $99$ Parsed:")
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:
output["ntlmv1"] = parse_ntlmv1(
args.ntlmv1,
key1=args.key1,
key2=args.key2,
json_mode=args.json
)
# Convert NTLMv1 -> $MSCHAPv2$
if args.mschapv2:
try:
output["mschapv2"] = parse_mschapv2(
args.mschapv2,
args.key1,
args.key2,
json_mode=args.json
)
except Exception as e:
print(f"[-] Failed to parse MSCHAPv2 input: {e}")
return
if args.hashcat:
# prefer ntlmv1 -> $99$ -> mschapv2, use whichever was parsed
ctx_key = next((k for k in ("ntlmv1", "$99$", "mschapv2") if k in output), None)
if ctx_key is None:
if not args.json:
print("[-] No parsed context to build hashcat lines. Provide --ntlmv1 / --99 / --mschapv2.")
else:
ctx = output[ctx_key]
ct1 = ctx.get("ct1")
ct2 = ctx.get("ct2")
challenge = ctx.get("challenge")
if ct1 and ct2 and challenge:
if args.json:
# attach to the same object we parsed (uniform for ntlmv1/$99$/mschapv2)
ctx["hash1"] = f"{ct1}:{challenge}"
ctx["hash2"] = f"{ct2}:{challenge}"
else:
print("\nTo crack with hashcat create a file with the following contents:")
print(f"{ct1}:{challenge}")
print(f"{ct2}:{challenge}\n")
print(f"echo \"{ct1}:{challenge}\" >> 14000.hash")
print(f"echo \"{ct2}:{challenge}\" >> 14000.hash\n")
else:
if not args.json:
print("[-] Missing ct1/ct2/challenge in context; cannot build hashcat lines.")
if args.json:
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()