-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathextract_ldap_hashes.py
More file actions
executable file
·100 lines (85 loc) · 3.09 KB
/
extract_ldap_hashes.py
File metadata and controls
executable file
·100 lines (85 loc) · 3.09 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
#!/usr/bin/env python3
import base64
import re
import sys
# Extract password hashes from a 389-ds database
#
# Dump the database with:
# $ dbscan -f /var/lib/dirsrv/slapd-DIRNAME/db/userRoot/id2entry.db
# Or
# $ ldapsearch -D "cn=Directory Manager" -x -w "Password123" "(userPassword=*)" krbCanonicalName uid cn userPassword ipaNTHash
# And then run this script on the output
next_line = False
user = ''
nt_hash = ''
complete_user = False
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <dumpfile>")
sys.exit(1)
try:
with open(sys.argv[1]) as f:
lines = f.readlines()
except FileNotFoundError:
print(f"File '{sys.argv[1]}' does not exist.")
sys.exit(1)
except:
print(f"Usage: {sys.argv[0]} <dumpfile>")
sys.exit(1)
for line in lines:
line = line.strip()
m = re.match('^\s+id [0-9]+$', line)
if m:
user = ''
nt_hash = ''
# Extract username.
# Doing this with regex is a bit hacky, but avoids any dependencies outside of stdlib
m = re.match('(krbCanonicalName|krbPrincipalName): (.*)$', line, re.IGNORECASE)
if m:
user = m.group(2)
encoded_hash = ''
# Not all account has a proper name, so fall back to UID, and then CN
if not user:
m = re.match('^\s*uid: (.*)$', line)
if m:
user = m.group(1)
encoded_hash = ''
else:
m = re.match('^\s*cn: (.*)$', line)
if m:
user = m.group(1)
encoded_hash = ''
# Check if we have an NT hash
m = re.match('ipaNTHash:: ([a-z0-9\+/=]+)', line, re.IGNORECASE)
if m:
nt_hash = '$NT$' + base64.b64decode(m.group(1)).hex()
# Extract password hash
m = re.match('userPassword:: ([a-z0-9\+/=]+)', line, re.IGNORECASE)
if m:
encoded_hash = m.group(1)
next_line = True
# Hash is usually split across multiple lines
elif next_line == True:
m = re.match('^\s*([a-z0-9\+/=]+)$', line, re.IGNORECASE)
if m:
encoded_hash += m.group(1).strip()
else:
next_line = False
complete_user = True
if complete_user:
decoded_hash = base64.b64decode(encoded_hash).decode('utf-8')
if '{PBKDF2_SHA256}' in decoded_hash:
binary_hash = base64.b64decode(decoded_hash[15:])
iterations = int.from_bytes(binary_hash[0:4], byteorder='big')
# John uses a slightly different base64 encodeding, with + replaced by .
salt = base64.b64encode(binary_hash[4:68], altchars=b'./').decode('utf-8').rstrip('=')
# 389-ds specifies an ouput (dkLen) length of 256 bytes, which is longer than John supports
# However, we can truncate this to 32 bytes and crack those
b64_hash = base64.b64encode(binary_hash[68:100], altchars=b'./').decode('utf-8').rstrip('=')
# Formatted for John
decoded_hash = f"$pbkdf2-sha256${iterations}${salt}${b64_hash}"
if nt_hash:
print(f'{user}:{nt_hash}')
print(f'{user}:{decoded_hash}')
complete_user = False
nt_hash = ''
user = ''