forked from clr2of8/DPAT
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dpat.py
executable file
·407 lines (368 loc) · 20.8 KB
/
dpat.py
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
#!/usr/bin/python
filename_for_html_report = "_DomainPasswordAuditReport.html"
folder_for_html_report = "DPAT Report"
filename_for_db_on_disk = "pass_audit.db"
compare_groups = []
# This should be False as it is only a shortcut used during development
speed_it_up = False
from pprint import pprint
from distutils.util import strtobool
import hashlib,binascii,cgi,sqlite3, argparse, os, io, webbrowser
parser = argparse.ArgumentParser(description='This script will perfrom a domain password audit based on an extracted NTDS file and password cracking output such as oclHashcat.')
parser.add_argument('-n','--ntdsfile', help='NTDS file name (output from SecretsDump.py)',required=True)
parser.add_argument('-c','--crackfile',help='Password Cracking output in the default form output by oclHashcat, such as oclHashcat.pot', required=True)
parser.add_argument('-o','--outputfile',help='The name of the HTML report output file, defaults to ' + filename_for_html_report, required=False, default=filename_for_html_report)
parser.add_argument('-d','--reportdirectory',help='Folder containing the output HTML files, defaults to ' + folder_for_html_report, required=False, default=folder_for_html_report)
parser.add_argument('-w','--writedb',help='Write the SQLite database info to disk for offline inspection instead of just in memory. Filename will be "' + filename_for_db_on_disk + '"', default=False, required=False, action='store_true')
parser.add_argument('-s','--sanitize',help='Sanitize the report by partially redacting passwords and hashes. Prepends the report directory with \"Sanitized - \"', default=False, required=False, action='store_true')
parser.add_argument('-g','--grouplists',help='The name of one or multiple files that contain lists of usernames in particular groups. The group names will be taken from the file name itself. The username list must be in the same format as found in the NTDS file such as some.ad.domain.com\username or it can be in the format output by using the PowerView Get-NetGroupMember function. Example: -g "Domain Admins.txt" "Enterprise Admins.txt"', nargs='*', required=False)
args = parser.parse_args()
ntds_file = args.ntdsfile
cracked_file = args.crackfile
filename_for_html_report = args.outputfile
folder_for_html_report = args.reportdirectory
if args.sanitize:
folder_for_html_report = "Sanitized - " + folder_for_html_report
if args.grouplists is not None:
for groupfile in args.grouplists:
compare_groups.append((os.path.splitext(os.path.basename(groupfile))[0],groupfile))
#create report folder if it doesn't already exist
if not os.path.exists(folder_for_html_report):
os.makedirs(folder_for_html_report)
#show only the first and last char of a password or a few more chars for a hash
def sanitize(pass_or_hash):
if not args.sanitize:
return pass_or_hash
else:
sanitized_string = pass_or_hash
lenp = len(pass_or_hash)
if lenp == 32:
sanitized_string = pass_or_hash[0:4] + "*"*(lenp-8) + pass_or_hash[lenp-5:lenp-1]
elif lenp > 2:
sanitized_string = pass_or_hash[0] + "*"*(lenp-2) + pass_or_hash[lenp-1]
return sanitized_string
class HtmlBuilder:
bodyStr = ""
def build_html_body_string(self, str):
self.bodyStr += str + "</br>\n"
def get_html(self):
return "<!DOCTYPE html>\n" + "<html>\n<head>\n<style>\ntable, th, td {border: 1px solid black; border-collapse: collapse; text-align: center;} th, td {padding: 2px;}</style>\n</head>\n" + "<body>\n" + self.bodyStr + "</html>\n" + "</body>\n"
def add_table_to_html(self,list,headers=[],col_to_not_escape=None):
html = '<table border="1">\n'
html += "<tr>"
for header in headers:
if header is not None:
html += "<th>" + str(header) + "</th>"
else:
html += "<th></th>"
html += "</tr>\n"
for line in list:
html += "<tr>"
col_num = 0
for column in line:
if column is not None:
col_data = column
if (headers[col_num] == "Password" or headers[col_num] == "NT Hash" or headers[col_num] == "LM Hash" or headers[col_num] == "Left Portion of Password" or headers[col_num] == "Right Portion of Password"):
col_data = sanitize(column)
if col_num != col_to_not_escape:
col_data = cgi.escape(str(col_data))
html += "<td>" + col_data + "</td>"
else:
html += "<td></td>"
col_num += 1
html += "</tr>\n"
html += "</table>"
self.build_html_body_string(html)
def write_html_report(self,filename):
f = open(os.path.join(folder_for_html_report,filename),"w")
f.write(self.get_html())
f.close()
return filename
hb = HtmlBuilder()
summary_table = []
summary_table_headers = ("Count","Description","More Info")
conn = sqlite3.connect(':memory:')
if args.writedb:
if os.path.exists(filename_for_db_on_disk):
os.remove(filename_for_db_on_disk)
conn = sqlite3.connect(filename_for_db_on_disk)
if speed_it_up:
conn = sqlite3.connect(filename_for_db_on_disk)
conn.text_factory = str
c = conn.cursor()
# nt2lmcrack functionality
# the all_casings functionality was taken from https://github.com/BBerastegui/foo/blob/master/casing.py
def all_casings(input_string):
if not input_string:
yield ""
else:
first = input_string[:1]
if first.lower() == first.upper():
for sub_casing in all_casings(input_string[1:]):
yield first + sub_casing
else:
for sub_casing in all_casings(input_string[1:]):
yield first.lower() + sub_casing
yield first.upper() + sub_casing
def crack_it(nt_hash,lm_pass):
password = None
for pwd_guess in all_casings(lm_pass):
hash = hashlib.new('md4', pwd_guess.encode('utf-16le')).digest()
if nt_hash == binascii.hexlify(hash):
password = pwd_guess
break
return password
if not speed_it_up:
# Create tables and indices
c.execute('''CREATE TABLE hash_infos
(username_full text, username text, lm_hash text, lm_hash_left text, lm_hash_right text, nt_hash text, password text, lm_pass_left text, lm_pass_right text, only_lm_cracked boolean)''')
c.execute("CREATE INDEX index_nt_hash ON hash_infos (nt_hash);")
c.execute("CREATE INDEX index_lm_hash_left ON hash_infos (lm_hash_left);")
c.execute("CREATE INDEX index_lm_hash_right ON hash_infos (lm_hash_right);")
c.execute("CREATE INDEX lm_hash ON hash_infos (lm_hash);")
c.execute("CREATE INDEX username ON hash_infos (username);")
# Create boolean column for each group
for group in compare_groups:
sql = "ALTER TABLE hash_infos ADD COLUMN \"" + group[0] + "\" boolean"
c.execute(sql)
# Read users from each group; groups_users is a dictionary with key = group name and value = list of users
groups_users = {}
for group in compare_groups:
user_domain = ""
user_name = ""
try:
fing = io.open(group[1], encoding='utf-16')
users = []
for line in fing:
if "MemberDomain" in line:
user_domain = (line.split(":")[1]).strip()
if "MemberName" in line:
user_name = (line.split(":")[1]).strip()
users.append(user_domain + "\\" + user_name)
except:
print "Doesn't look like the Group Files are in the form output by PowerView, assuming the files are already in domain\username list form"
# If the users array is empty, assume the file was not in the PowerView PowerShell script output format that you get from running:
# Get-NetGroupMember -GroupName "Enterprise Admins" -Domain "some.domain.com" -DomainController "DC01.some.domain.com" > Enterprise Admins.txt
# You can list domain controllers for use in the above command with Get-NetForestDomain
if len(users) == 0:
fing = open(group[1])
users = []
for line in fing:
users.append(line.rstrip("\n"))
fing.close()
groups_users[group[0]] = users
# Read in NTDS file
fin = open(ntds_file)
for line in fin:
vals = line.rstrip("\n").split(':')
usernameFull = vals[0]
lm_hash = vals[2]
lm_hash_left = lm_hash[0:16]
lm_hash_right = lm_hash[16:32]
nt_hash = vals[3]
username = usernameFull.split('\\')[-1]
# Insert a row of data
c.execute("INSERT INTO hash_infos (username_full, username, lm_hash , lm_hash_left , lm_hash_right , nt_hash) VALUES (?,?,?,?,?,?)", (usernameFull, username, lm_hash, lm_hash_left, lm_hash_right, nt_hash))
fin.close()
# update group membership flags
for group in groups_users:
for user in groups_users[group]:
sql = "UPDATE hash_infos SET \"" + group + "\" = 1 WHERE username_full = \"" + user + "\""
c.execute(sql)
# read in POT file
fin = open(cracked_file)
for lineT in fin:
line = lineT.rstrip('\r\n')
colon_index = line.find(":")
hash = line[0:colon_index]
# Stripping $NT$ and $LM$ that is included in John the Ripper output by default
hash = hash.lstrip("$NT$");
hash = hash.lstrip("$LM$");
password = line[colon_index+1:len(line)]
lenxx = len(hash)
if lenxx == 32: # An NT hash
c.execute("UPDATE hash_infos SET password = ? WHERE nt_hash = ?", (password, hash))
elif lenxx == 16: # An LM hash, either left or right
c.execute("UPDATE hash_infos SET lm_pass_left = ? WHERE lm_hash_left = ?", (password, hash))
c.execute("UPDATE hash_infos SET lm_pass_right = ? WHERE lm_hash_right = ?", (password, hash))
else:
print "What kind of a hash is this??"
fin.close()
# Do additional LM cracking
c.execute('SELECT nt_hash,lm_pass_left,lm_pass_right FROM hash_infos WHERE (lm_pass_left is not NULL or lm_pass_right is not NULL) and password is NULL and lm_hash is not "aad3b435b51404eeaad3b435b51404ee" group by nt_hash')
list = c.fetchall()
count = len(list)
print "Cracking %d NT Hashes where only LM Hash was cracked (aka lm2ntcrack functionality)" % count
for pair in list:
lm_pwd = ""
if pair[1] is not None:
lm_pwd += pair[1]
if pair[2] is not None:
lm_pwd += pair[2]
password = crack_it(pair[0],lm_pwd)
if password is not None:
c.execute('UPDATE hash_infos SET only_lm_cracked = 1, password = \'' + password.replace("'","''") + '\' WHERE nt_hash = \'' + pair[0] + '\'')
count-=1
# Total number of hashes in the NTDS file
c.execute('SELECT username_full,password,LENGTH(password) as plen,nt_hash,only_lm_cracked FROM hash_infos ORDER BY plen DESC, password')
list = c.fetchall()
num_hashes = len(list)
hbt = HtmlBuilder()
hbt.add_table_to_html(list,["Username","Password","Password Length","NT Hash","Only LM Cracked"])
filename = hbt.write_html_report("all hashes.html")
summary_table.append((num_hashes,"Password Hashes","<a href=\"" + filename + "\">Details</a>"))
# Total number of UNIQUE hashes in the NTDS file
c.execute('SELECT count(DISTINCT nt_hash) FROM hash_infos')
num_unique_nt_hashes = c.fetchone()[0]
summary_table.append((num_unique_nt_hashes,"Unique Password Hashes",None))
# Number of users whose passwords were cracked
c.execute('SELECT count(*) FROM hash_infos where password is not NULL')
num_passwords_cracked = c.fetchone()[0]
summary_table.append((num_passwords_cracked,"Passwords Discovered Through Cracking",None))
# Number of UNIQUE passwords that were cracked
c.execute('SELECT count(Distinct password) FROM hash_infos where password is not NULL')
num_unique_passwords_cracked = c.fetchone()[0]
summary_table.append((num_unique_passwords_cracked,"Unique Passwords Discovered Through Cracking",None))
# Percentage of all passwords cracked and percentage of unique passwords cracked
percent_cracked_unique = num_unique_passwords_cracked/float(num_unique_nt_hashes)*100
percent_all_cracked = num_passwords_cracked/float(num_hashes)*100
summary_table.append(("%0.1f" % percent_all_cracked,"Percent of Passwords Cracked","<a href=\"" + filename + "\">Details</a>"))
summary_table.append(("%0.1f" % percent_cracked_unique,"Percent of Unique Passwords Cracked","<a href=\"" + filename + "\">Details</a>"))
# Group Membership Details and number of passwords cracked for each group
for group in compare_groups:
c.execute("SELECT username_full,nt_hash FROM hash_infos WHERE \"" + group[0] + "\" = 1")
list = c.fetchall() #this list contains the username_full and nt_hash of all users in this group
num_groupmembers = len(list)
new_list = []
for tuple in list: # the tuple is (username_full, nt_hash, lm_hash)
c.execute("SELECT username_full FROM hash_infos WHERE nt_hash = \"" + tuple[1] + "\"")
users_list = c.fetchall()
if len(users_list) < 30:
string_of_users = (', '.join(''.join(elems) for elems in users_list))
new_tuple = tuple + (string_of_users,)
else:
new_tuple = tuple + ("Too Many to List",)
new_tuple += (len(users_list),)
c.execute("SELECT password,lm_hash FROM hash_infos WHERE nt_hash = \"" + tuple[1] + "\" LIMIT 1")
result = c.fetchone()
new_tuple += (result[0],)
# Is the LM Hash stored for this user?
if result[1] != "aad3b435b51404eeaad3b435b51404ee":
new_tuple += ("Yes",)
else:
new_tuple += ("No",)
new_list.append(new_tuple)
headers = ["Username","NT Hash","Users Sharing this Hash", "Share Count","Password","Non-Blank LM Hash?"]
hbt = HtmlBuilder()
hbt.add_table_to_html(new_list, headers)
filename = hbt.write_html_report(group[0] + " members.html")
summary_table.append((num_groupmembers,"Members of \"%s\" group" % group[0],"<a href=\"" + filename + "\">Details</a>"))
c.execute("SELECT username_full, LENGTH(password) as plen, password, only_lm_cracked FROM hash_infos WHERE \"" + group[0] + "\" = 1 and password is not NULL and password is not '' ORDER BY plen")
group_cracked_list = c.fetchall()
num_groupmembers_cracked = len(group_cracked_list)
headers = ["Username of \"" + group[0] + "\" Member","Password Length","Password","Only LM Cracked"]
hbt = HtmlBuilder()
hbt.add_table_to_html(group_cracked_list, headers)
filename = hbt.write_html_report(group[0] + " cracked passwords.html")
summary_table.append((num_groupmembers_cracked,"\"%s\" Passwords Cracked" % group[0],"<a href=\"" + filename + "\">Details</a>"))
# Number of LM hashes in the NTDS file, excluding the blank value
c.execute('SELECT count(*) FROM hash_infos where lm_hash is not "aad3b435b51404eeaad3b435b51404ee"')
summary_table.append((c.fetchone()[0],"LM Hashes (Non-blank)",None))
# Number of UNIQUE LM hashes in the NTDS, excluding the blank value
c.execute('SELECT count(DISTINCT lm_hash) FROM hash_infos WHERE lm_hash is not "aad3b435b51404eeaad3b435b51404ee"')
summary_table.append((c.fetchone()[0],"Unique LM Hashes (Non-blank)",None))
# Number of passwords that are LM cracked for which you don't have the exact (case sensitive) password.
c.execute('SELECT lm_hash, lm_pass_left, lm_pass_right, nt_hash FROM hash_infos WHERE (lm_pass_left is not "" or lm_pass_right is not "") and password is NULL and lm_hash is not "aad3b435b51404eeaad3b435b51404ee" group by lm_hash')
list = c.fetchall()
num_lm_hashes_cracked_where_nt_hash_not_cracked = len(list)
output = "WARNING there were %d unique LM hashes for which you do not have the password." % num_lm_hashes_cracked_where_nt_hash_not_cracked
if num_lm_hashes_cracked_where_nt_hash_not_cracked != 0:
hbt = HtmlBuilder()
headers = ["LM Hash","Left Portion of Password","Right Portion of Password","NT Hash"]
hbt.add_table_to_html(list,headers)
filename = hbt.write_html_report("lm_noncracked.html")
hb.build_html_body_string(output + ' <a href="' + filename + '">Details</a>')
output2 = "</br> Cracking these to their 7-character upcased representation is easy with oclHashcat and this tool will determine the correct case and concatenate the two halves of the password for you!</br></br> Try this oclHashcat command to crack all LM hashes:</br> <strong>./oclHashcat64.bin -m 3000 -a 3 customer.ntds -1 ?a ?1?1?1?1?1?1?1 --increment</strong></br></br> Or for John, try this:</br> <strong>john --format=LM customer.ntds</strong></br>"
hb.build_html_body_string(output2)
# Count and List of passwords that were only able to be cracked because the LM hash was available, includes usernames
c.execute('SELECT username_full,password,LENGTH(password) as plen,only_lm_cracked FROM hash_infos WHERE only_lm_cracked = 1 ORDER BY plen')
list = c.fetchall()
hbt = HtmlBuilder()
headers = ["Username","Password","Password Length","Only LM Cracked"]
hbt.add_table_to_html(list,headers)
filename = hbt.write_html_report("users_only_cracked_through_lm.html")
summary_table.append((len(list),"Passwords Only Cracked via LM Hash","<a href=\"" + filename + "\">Details</a>"))
c.execute('SELECT COUNT(DISTINCT nt_hash) FROM hash_infos WHERE only_lm_cracked = 1')
summary_table.append((c.fetchone()[0],"Unique LM Hashes Cracked Where NT Hash was Not Cracked",None))
# Password length statistics
c.execute('SELECT LENGTH(password) as plen,COUNT(password) FROM hash_infos WHERE plen is not NULL and plen is not 0 GROUP BY plen ORDER BY plen')
list = c.fetchall()
counter = 0
for tuple in list:
length = str(tuple[0])
c.execute('SELECT username FROM hash_infos WHERE LENGTH(password) = ' + length)
usernames = c.fetchall()
hbt = HtmlBuilder()
headers = ["Users with a password length of " + length]
hbt.add_table_to_html(usernames,headers)
filename = hbt.write_html_report(str(counter) + "length_usernames.html")
list[counter]+=("<a href=\"" + filename + "\">Details</a>",)
counter += 1
hbt = HtmlBuilder()
headers = ["Password Length","Count","Details"]
hbt.add_table_to_html(list,headers,2)
c.execute('SELECT COUNT(password) as count, LENGTH(password) as plen FROM hash_infos WHERE plen is not NULL and plen is not 0 GROUP BY plen ORDER BY count DESC')
list = c.fetchall()
headers = ["Count","Password Length"]
hbt.add_table_to_html(list,headers)
filename = hbt.write_html_report("password_length_stats.html")
summary_table.append((None,"Password Length Stats","<a href=\"" + filename + "\">Details</a>"))
# Top Ten Passwords Used
c.execute('SELECT password,COUNT(password) as count FROM hash_infos WHERE password is not NULL and password is not "" GROUP BY password ORDER BY count DESC LIMIT 20')
list = c.fetchall()
hbt = HtmlBuilder()
headers = ["Password","Count"]
hbt.add_table_to_html(list,headers)
filename = hbt.write_html_report("top_password_stats.html")
summary_table.append((None,"Top Password Use Stats","<a href=\"" + filename + "\">Details</a>"))
# Password Reuse Statistics (based only on NT hash)
c.execute('SELECT nt_hash, COUNT(nt_hash) as count, password FROM hash_infos WHERE nt_hash is not "31d6cfe0d16ae931b73c59d7e0c089c0" GROUP BY nt_hash ORDER BY count DESC LIMIT 20')
list = c.fetchall()
counter = 0
for tuple in list:
c.execute('SELECT username FROM hash_infos WHERE nt_hash = \"' + tuple[0] + '\"')
usernames = c.fetchall()
password = tuple[2]
if password is None:
password = ""
hbt = HtmlBuilder()
headers = ["Users Sharing a Hash:Password of " + sanitize(tuple[0]) + ":" + sanitize(password)]
hbt.add_table_to_html(usernames,headers)
filename = hbt.write_html_report(str(counter) + "reuse_usernames.html")
list[counter]+=("<a href=\"" + filename + "\">Details</a>",)
counter += 1
hbt = HtmlBuilder()
headers = ["NT Hash","Count","Password","Details"]
hbt.add_table_to_html(list,headers,3)
filename = hbt.write_html_report("password_reuse_stats.html")
summary_table.append((None,"Password Reuse Stats","<a href=\"" + filename + "\">Details</a>"))
# Write out the main report page
hb.add_table_to_html(summary_table,summary_table_headers,2)
hb.write_html_report(filename_for_html_report)
print "The Report has been written to the \"" + filename_for_html_report + "\" file in the \"" +folder_for_html_report + "\" directory"
# Save (commit) the changes and close the database connection
conn.commit()
conn.close()
# prompt user to open the report
# the code to prompt user to open the file was borrowed from the EyeWitness tool https://github.com/ChrisTruncer/EyeWitness
print 'Would you like to open the report now? [Y/n]',
while True:
try:
response = raw_input().lower()
if ((response is "") or (strtobool(response))):
webbrowser.open(os.path.join("file://" + os.getcwd(),folder_for_html_report,filename_for_html_report))
break
else:
break
except ValueError:
print "Please respond with y or n",