Last active
February 3, 2023 02:29
-
-
Save SoftPoison/cbe78f3968af0c6da915f141069b2a9b to your computer and use it in GitHub Desktop.
Maps cracked hashes with a dcsync log to create a handy readout of compromised accounts + stats
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from collections import Counter | |
from functools import reduce | |
import sys | |
from typing import List | |
class LoginCreds: | |
name: str | |
username: str | |
uac: str | |
sid: str | |
pwhash: str | |
password: str | |
def __init__(self, lines): | |
lo = lines[1] | |
if lines[0].startswith('Object'): | |
lo = lines[0] | |
self.name = lo.split(': ', 1)[1] | |
self.username = lines[5].split(': ', 1)[1] | |
self.uac = lines[6].split(': ', 1)[1] | |
self.sid = lines[7].split(': ', 1)[1] | |
self.pwhash = lines[11].split(': ', 1)[1] | |
self.password = None | |
def __str__(self) -> str: | |
pw = self.password if self.password != None else self.pwhash | |
ret = '' | |
ret += self.username + ':' + pw + '\n' | |
ret += '\tName: ' + self.name + '\n' | |
ret += '\tUAC: ' + self.uac + '\n' | |
ret += '\tSID: ' + self.sid + '\n' | |
return ret | |
def is_disabled(self) -> bool: | |
return 'ACCOUNTDISABLE' in self.uac | |
def remove_duplicate_usernames(creds: List[LoginCreds]): | |
usernames: List[str] = [] | |
def uniquify(c: LoginCreds): | |
if c.username in usernames: | |
return False | |
usernames.append(c.username) | |
return True | |
# reversed sid sorting, so newer accounts come first | |
rev = sorted(creds, key=lambda x: int(x.sid.split('-')[-1]), reverse=True) | |
return list(filter(uniquify, rev)) | |
def base_password(password: str) -> str: | |
base = password.lower() | |
# remove starting non-alpha chars | |
offset = 0 | |
for i in range(len(base)): | |
if base[i].isalpha(): | |
break | |
offset += 1 | |
base = base[offset:] | |
# remove ending non-alpha chars | |
offset = len(base) | |
for i in range(len(base)-1, 0, -1): | |
if base[i].isalpha(): | |
break | |
offset -= 1 | |
base = base[:offset] | |
# simple common visual substitutions | |
base = base.replace("!", "i") | |
base = base.replace("@", "a") | |
base = base.replace("$", "s") | |
base = base.replace("|", "l") | |
base = base.replace("0", "o") | |
base = base.replace("1", "i") | |
base = base.replace("3", "e") | |
base = base.replace("4", "a") | |
base = base.replace("5", "s") | |
base = base.replace("7", "t") | |
base = base.replace("8", "b") | |
return base | |
if __name__ == '__main__': | |
if len(sys.argv) != 3: | |
print(f"Usage: {sys.argv[0]} <cracked_hashes.txt> <dcsync.log>") | |
print(" cracked_hashes.txt: a newline separated list of hashes and passwords of the form hash:password") | |
print(" dcsync.log: a log file generated from mimikatz's `log dcsync.log; lsadump::dcsync /all`") | |
exit(1) | |
hashes = [] | |
with open(sys.argv[1], 'r') as f: | |
hashes = f.read().splitlines() | |
hashes = list(filter(lambda x: x.strip() != '', hashes)) | |
hash_pw_map = dict(map(lambda x: x.split(':', 1), hashes)) | |
dcsync_log = "" | |
dcsync_log_lines = [] | |
with open(sys.argv[2], 'r') as f: | |
dcsync_log = f.read() | |
dcsync_log_lines = dcsync_log.splitlines() | |
creds = [] | |
for i in range(len(dcsync_log_lines)): | |
line = dcsync_log_lines[i] | |
if not line.startswith(' Hash NTLM: '): | |
continue | |
cred = LoginCreds(dcsync_log_lines[i-11:i+1]) | |
creds.append(cred) | |
cracked_creds = [] | |
for c in creds: | |
for h in hash_pw_map: | |
if c.pwhash == h: | |
c.password = hash_pw_map[h] | |
cracked_creds.append(c) | |
break | |
valid_creds = list(filter(lambda c: not c.is_disabled(), cracked_creds)) | |
active_accounts = list(filter(lambda c: not c.is_disabled(), creds)) | |
real_accounts = list(filter(lambda c: not c.username.endswith('$') and c.username != "krbtgt", creds)) # remove machine accounts | |
real_accounts = remove_duplicate_usernames(real_accounts) | |
real_active_accounts = list(filter(lambda c: not c.username.endswith('$') and c.username != "krbtgt", active_accounts)) # remove machine accounts | |
real_active_accounts = remove_duplicate_usernames(real_active_accounts) | |
valid_creds = remove_duplicate_usernames(valid_creds) # remove any accounts with duplicate usernames | |
machine_accounts = list(filter(lambda c: c.username.endswith('$'), valid_creds)) | |
cracked_active_percentage = len(valid_creds)/len(real_active_accounts) * 100 | |
cracked_all_percentage = len(cracked_creds)/len(real_accounts) * 100 | |
print('Summary:') | |
print(f'\tCracked {cracked_active_percentage:.1f}% of active user accounts ({len(valid_creds)}/{len(real_active_accounts)})') | |
print(f'\tCracked {cracked_all_percentage:.1f}% of ALL user accounts ({len(cracked_creds)}/{len(real_accounts)})') | |
print(f'\tCracked {len(machine_accounts)} machine account(s)') | |
print() | |
count = Counter(map(lambda c: c.password, valid_creds)) | |
most_common = count.most_common(10) | |
pad_len_0 = reduce(lambda a, x: max(a, len(x[0])), most_common, 0) | |
pad_len_0 = max(pad_len_0, len("Password")) | |
pad_len_1 = reduce(lambda a, x: max(a, len(str(x[1]))), most_common, 0) | |
pad_len_1 = max(pad_len_1, len("Frequency")) | |
print('Top 10 most common passwords:') | |
print(f'\t| {"Password".ljust(pad_len_0)} | {"Frequency".ljust(pad_len_1)} |') | |
print(f'\t|{"-"*(pad_len_0+2)}|{"-"*(pad_len_1+2)}|') | |
for x in most_common: | |
print(f'\t| {x[0].ljust(pad_len_0)} | {str(x[1]).ljust(pad_len_1)} |') | |
print() | |
count = Counter(map(lambda c: base_password(c.password), valid_creds)) | |
most_common = count.most_common(10) | |
pad_len_0 = reduce(lambda a, x: max(a, len(x[0])), most_common, 0) | |
pad_len_0 = max(pad_len_0, len("Password base")) | |
pad_len_1 = reduce(lambda a, x: max(a, len(str(x[1]))), most_common, 0) | |
pad_len_1 = max(pad_len_1, len("Frequency")) | |
print('Top 10 most common password bases:') | |
print(f'\t| {"Password base".ljust(pad_len_0)} | {"Frequency".ljust(pad_len_1)} |') | |
print(f'\t|{"-"*(pad_len_0+2)}|{"-"*(pad_len_1+2)}|') | |
for x in most_common: | |
print(f'\t| {x[0].ljust(pad_len_0)} | {str(x[1]).ljust(pad_len_1)} |') | |
print() | |
print('All active user accounts:') | |
print() | |
valid_creds.sort(key=lambda x: int(x.sid.split('-')[-1])) | |
for c in valid_creds: | |
print(c) | |
for c in machine_accounts: | |
print(c) | |
print() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from collections import Counter | |
from functools import reduce | |
import sys | |
from typing import List | |
class LoginCreds: | |
name: str | |
username: str | |
uac: str | |
sid: str | |
pwhash: str | |
password: str | |
def __init__(self, lines): | |
lo = lines[1] | |
if lines[0].startswith('Object'): | |
lo = lines[0] | |
self.name = lo.split(': ', 1)[1] | |
self.username = lines[5].split(': ', 1)[1].lower() | |
self.uac = lines[6].split(': ', 1)[1] | |
self.sid = lines[7].split(': ', 1)[1] | |
self.pwhash = lines[11].split(': ', 1)[1] | |
self.password = None | |
def __str__(self) -> str: | |
pw = self.password if self.password != None else self.pwhash | |
ret = '' | |
ret += self.username + ':' + pw + '\n' | |
ret += '\tName: ' + self.name + '\n' | |
ret += '\tUAC: ' + self.uac + '\n' | |
ret += '\tSID: ' + self.sid + '\n' | |
return ret | |
def is_disabled(self) -> bool: | |
return 'ACCOUNTDISABLE' in self.uac | |
def remove_duplicate_usernames(creds: List[LoginCreds]): | |
usernames: List[str] = [] | |
def uniquify(c: LoginCreds): | |
if c.username in usernames: | |
return False | |
usernames.append(c.username) | |
return True | |
# reversed sid sorting, so newer accounts come first | |
rev = sorted(creds, key=lambda x: int(x.sid.split('-')[-1]), reverse=True) | |
return list(filter(uniquify, rev)) | |
if __name__ == '__main__': | |
if len(sys.argv) != 3: | |
print(f"Usage: {sys.argv[0]} <cracked_hashes.txt> <dcsync.log>") | |
print(" cracked_hashes.txt: a newline separated list of hashes and passwords of the form hash:password") | |
print(" dcsync.log: a log file generated from mimikatz's `log dcsync.log; lsadump::dcsync /all`") | |
exit(1) | |
hashes = [] | |
with open(sys.argv[1], 'r') as f: | |
hashes = f.read().splitlines() | |
hashes = list(filter(lambda x: x.strip() != '', hashes)) | |
hash_pw_map = dict(map(lambda x: x.split(':', 1), hashes)) | |
dcsync_log = "" | |
dcsync_log_lines = [] | |
with open(sys.argv[2], 'r') as f: | |
dcsync_log = f.read() | |
dcsync_log_lines = dcsync_log.splitlines() | |
creds = [] | |
for i in range(len(dcsync_log_lines)): | |
line = dcsync_log_lines[i] | |
if not line.startswith(' Hash NTLM: '): | |
continue | |
cred = LoginCreds(dcsync_log_lines[i-11:i+1]) | |
creds.append(cred) | |
creds = remove_duplicate_usernames(creds) | |
username_to_cred: dict[str, LoginCreds] = {} | |
for c in creds: | |
username_to_cred[c.username] = c | |
admins = [] | |
for c in creds: | |
if not c.username.startswith('admin.'): | |
continue | |
standard = c.username.split('admin.')[1] | |
if username_to_cred.get(standard) == None: | |
continue | |
if c.pwhash == username_to_cred[standard].pwhash: | |
admins.append(c.username) | |
if len(admins) > 0: | |
print("Admins with the same password as their standard account:") | |
for a in admins: | |
print(a) | |
print() | |
hashes_to_accs: dict[str, list[LoginCreds]] = {} | |
for c in creds: | |
h = c.pwhash | |
if hashes_to_accs.get(h) == None: | |
hashes_to_accs[h] = [] | |
hashes_to_accs[h].append(c) | |
samesies: list[str] = [] | |
for h, cs in hashes_to_accs.items(): | |
if len(cs) > 1: | |
s = f"{len(cs):03}: {h} " | |
if hash_pw_map.get(h) != None: | |
s += f"({hash_pw_map[h]}) " | |
samesies.append(s) | |
samesies.sort() | |
samesies.reverse() | |
samesies = [x.strip('0') for x in samesies] | |
print("Number of accounts with the same password hash:") | |
for h in samesies: | |
print(h) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment