Skip to content

Instantly share code, notes, and snippets.

@SoftPoison
Last active February 3, 2023 02:29
Show Gist options
  • Save SoftPoison/cbe78f3968af0c6da915f141069b2a9b to your computer and use it in GitHub Desktop.
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
#!/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()
#!/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