#!/usr/bin/python # -*- coding: utf-8 -*- from fuzzywuzzy import process from ..ReportTemplate import ReportTemplate import datetime class Report(ReportTemplate): """ This report takes a dumped NTDS file and a cracked NTDS file (from hashcat). It will then match up users and passwords, as well as do a password strength audit. """ table_delim = u"λ" table_newline = u"Δ" name = "ADUserCreds" def __init__(self, db): pass def set_options(self): super(Report, self).set_options() self.options.add_argument( "-ih", "--hashes", help="Input ntds export containing hashes" ) self.options.add_argument( "-ic", "--cracked", help="Input file containing cracked hashes (hash:plaintext)", ) self.options.add_argument( "-o1", "--user_passwords", help="Report of all user names with plaintext passwords.", action="store_true", ) self.options.add_argument( "--columns", help="Print out user/passwords by columns (Useful for screenshots)", action="store_true", ) self.options.add_argument( "-o2", "--lm_hashes", help="export of all lines containing lm hashes", action="store_true", ) self.options.add_argument( "-o3", "--password_audit", help="Display a password audit of the compromised data", action="store_true", ) self.options.add_argument( "-d", "--delimiter", help="Delimiter for output of user passwords (Default :)", default=":", ) self.options.add_argument( "-k", "--keywords", help="Additional keywords to search for the export report (Comma separated", ) def run(self, args): td = self.table_delim nd = self.table_newline results = [] length_count = {} hash_map = {} pw_count = {} hash_count = {} if args.user_passwords: if not (args.hashes and args.cracked): print("Error - you need to supply NTDS hashes and your crack results") return user_map = {} hash_map = {} cracked_data = open(args.cracked, "rb").read() if cracked_data[:2] == b"\xff\xfe": cracked_data = ( cracked_data[2:].replace(b"\x00", b"").replace(b"\r", b"") ) cracked_data = cracked_data.decode() for c in cracked_data.split("\n"): h = c[:32] pw = c[33:] hash_map[h] = pw with open(args.hashes) as hash_file: for l in hash_file: try: username = l.split(":")[0] h = l.split(":")[3] if hash_map.get(h, False): user_map[username] = hash_map[h] except Exception: print("couldn't parse", l) if args.columns: maxlen = max([len(a) for a in user_map.keys()]) + 1 for k in sorted(user_map.keys()): results.append("%s%s%s" % (k, (maxlen - len(k)) * " ", user_map[k])) else: for k in sorted(user_map.keys()): results.append("%s%s%s" % (k, args.delimiter, user_map[k])) elif args.lm_hashes: with open(args.hashes) as hash_file: for l in hash_file: lm = l.split(":")[2] if lm != "aad3b435b51404eeaad3b435b51404ee": results.append(l.strip()) results.sort() elif args.password_audit: if not (args.hashes and args.cracked): print("Error - you need to supply NTDS hashes and your crack results") return cracked_data = open(args.cracked).read() if cracked_data[:2] == "\xff\xfe": cracked_data = cracked_data[2:].replace("\x00", "").replace("\r", "") for c in cracked_data.split("\n"): h = c[:32] pw = c[33:] if pw[:5] == "$HEX[" and pw[-1] == "]": pw = pw[5:-1].lower().decode("hex") hash_map[h] = pw with open(args.hashes) as hash_file: for l in hash_file: try: h = l.split(":")[3] if hash_map.get(h, False): if not pw_count.get(hash_map[h], False): pw_count[hash_map[h]] = { "count": 0, "length": len(hash_map[h]), } pw_count[hash_map[h]]["count"] += 1 if not length_count.get(len(hash_map[h]), False): length_count[len(hash_map[h])] = {"count": 0, "pws": []} length_count[len(hash_map[h])]["count"] += 1 length_count[len(hash_map[h])]["pws"].append(hash_map[h]) if not hash_count.get(h, False): hash_count[h] = 0 hash_count[h] += 1 except Exception: print("Couldn't parse", l) # Build out the stats results.append("\t\tGeneral Statistics") results.append("Count%sDescription" % td) total_hashes = sum([hash_count[c] for c in hash_count.keys()]) total_hashes_unique = len(hash_count.keys()) total_pws = sum([pw_count[_h]["count"] for _h in pw_count.keys()]) total_pws_unique = len(pw_count.keys()) total_cracked = int(total_pws / float(total_hashes) * 1000) / 10.0 total_cracked_unique = ( int(total_pws_unique / float(total_hashes_unique) * 1000) / 10.0 ) results.append("%s%sPassword Hashes" % (total_hashes, td)) results.append("%s%sUnique Password Hashes" % (total_hashes_unique, td)) results.append( "%s%sPasswords Discovered Through Cracking" % (total_pws, td) ) results.append( "%s%sUnique Passwords Discovered Through Cracking" % (total_pws_unique, td) ) results.append("%s%sPercent of Passwords Cracked" % (total_cracked, td)) results.append( "%s%sPercent of Unique Passwords Cracked" % (total_cracked_unique, td) ) shortest_passwords = length_count[sorted(length_count.keys())[0]]["pws"] longest_passwords = length_count[sorted(length_count.keys())[-1]]["pws"] if len(shortest_passwords) > 1: results.append( "%s%sShortest Passwords" % (nd.join(list(set(shortest_passwords[:3]))), td) ) else: results.append("%s%sShortest Password" % (shortest_passwords[0], td)) if len(longest_passwords) > 1: results.append( "%s%sLongest Passwords" % (nd.join(list(set(longest_passwords[:3]))), td) ) else: results.append("%s%sLongest Password" % (longest_passwords[0], td)) months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ] seasons = ["Winter", "Spring", "Summer", "Fall"] years = [ str(y) for y in range( datetime.datetime.now().year, datetime.datetime.now().year - 15, -1 ) ] results.append( "%s%sPasswords Containing a Season" % (self.search_term(seasons, pw_count), td) ) results.append( "%s%sPasswords Containing a Month" % (self.search_term(months, pw_count), td) ) results.append( "%s%sPasswords Containing a Year" % (self.search_term(years, pw_count), td) ) if args.keywords: keywords = [k.strip() for k in args.keywords.split(",")] for k in keywords: results.append( '%s%s Passwords Containing "%s"' % (self.search_term(k, pw_count), td, k) ) results.append("") results.append("\t\tPassword Length Statistics") results.append("Password Length%sCount" % td) for l in sorted(length_count.keys()): results.append("%s%s%s" % (l, td, length_count[l]["count"])) count_totals = [] for k in pw_count.keys(): count_totals.append([pw_count[k]["count"], k]) top20 = sorted(count_totals)[::-1][:20] results.append("") results.append("\t\tTop %s Passwords" % len(top20)) results.append("Password%sCount" % td) for t in top20: results.append( "%s%s%s" % (t[1].replace("\n", "").replace("\r", ""), td, t[0]) ) self.process_output(results, args) def search_term(self, txt, pw_count): pws = pw_count.keys() if type(txt) == str: txt = [txt] total_matches = 0 for t in txt: matches = [r[0] for r in process.extract(t, pws, limit=None) if r[1] > 75] total_matches += sum([pw_count[p]["count"] for p in matches]) return total_matches