├── README.md └── pw_spy.py /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____ __ __ _____ 3 | | __ \ \ / / / ____| 4 | | |__) \ \ /\ / / | (___ _ __ _ _ 5 | | ___/ \ \/ \/ / \___ \| '_ \| | | | 6 | | | \ /\ / ____) | |_) | |_| | 7 | |_| \/ \/ |_____/| .__/ \__, | 8 | | | __/ | 9 | |_| |___/ 10 | ``` 11 | # PW_Spy 12 | This is a tool to help with analysis after a full password audit. To use it simply call ```./pw_analysis.py``` and provide it with the list of hashes attempted and potfile from your engagement. 13 | So far it will strip out the basewords from the plaintext passwords in the potfile and count the occurances of those. It will also look for any re-used hashes in the list of hashes attempted. Finally it will create the most common password masks observed from the plaintext passwords to identify patterns in the passwords you were able to crack from the environment. 14 | 15 | ## Example 16 | ```./pw_spy.py full_list_of_attempted_hashes.txt engagement_potfile.pot``` 17 | 18 | ## Considerations 19 | It would be best to send this into an output file: 20 | 21 | ```./pw_spy.py full_list_of_attempted_hashes.txt engagement_potfile.pot > analysis.txt``` 22 | 23 | If you don't want a specific piece of the analysis for some reason simply comment out the function call at the bottom of the file. 24 | 25 | This assumes you used Hashcat and your full hashlist is stripped with one hash per line. The potfile is a raw potfile from Hashcat. 26 | 27 | ## TODO 28 | 1. I will be adding more pieces as I get to it, this is more for personal use at this point. 29 | -------------------------------------------------------------------------------- /pw_spy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import re 5 | import operator 6 | import argparse 7 | from collections import Counter 8 | 9 | 10 | parser = argparse.ArgumentParser() 11 | 12 | parser.add_argument("-pot", "--potfile", dest = "potfile", help="Pot file") 13 | parser.add_argument("-hash", "--hashlist", dest = "hashlist", help="Hash List") 14 | 15 | args = parser.parse_args() 16 | #repeated hashes (from full hashlist) 17 | repeated_hashes = [] 18 | 19 | #plaintext passwords (from potfile) 20 | plaintext_passwords = [] 21 | 22 | #cracked hashes (from potfile) 23 | cracked_hashes = [] 24 | 25 | #letters 26 | letters_regex = re.compile('[^a-zA-Z]') 27 | 28 | #potfile 29 | potfile = open(args.potfile) 30 | 31 | #potfile raw 32 | potfile_raw = [] 33 | 34 | #full hashlists 35 | if args.hashlist is not None: 36 | full_hashlist = open(args.hashlist) 37 | 38 | 39 | print("""\ 40 | ____ __ __ _____ 41 | | __ \ \ / / / ____| 42 | | |__) \ \ /\ / / | (___ _ __ _ _ 43 | | ___/ \ \/ \/ / \___ \| '_ \| | | | 44 | | | \ /\ / ____) | |_) | |_| | 45 | |_| \/ \/ |_____/| .__/ \__, | 46 | | | __/ | 47 | |_| |___/ 48 | """) 49 | 50 | if len(sys.argv) < 3: 51 | print("Usage is: ") 52 | sys.exit() 53 | 54 | #split out the potfile to get hashes and plaintext passwords 55 | def split_pot(): 56 | print('###################### SPLITTING POT FILE ##################\n') 57 | for password in potfile: 58 | potfile_raw.append(password.strip()) 59 | split_pw = password.split(":", 1) 60 | hash_string, pw_string = split_pw 61 | cracked_hashes.append(hash_string) 62 | plaintext_passwords.append(pw_string) 63 | print('...DONE') 64 | 65 | #Find how many times a password was reused 66 | def pw_reuse(): 67 | split_char = ":" 68 | split = 3 69 | print("\n\n\n###################### PASSWORD REUSE ######################\n") 70 | if args.hashlist is not None: 71 | for word in full_hashlist: 72 | stripped_hash = word.strip() 73 | ripped_hash = stripped_hash.split(split_char) 74 | fixed_hash = split_char.join(ripped_hash[split:]) 75 | final_hash = fixed_hash.split(":",1) 76 | repeated_hashes.append(final_hash[0]) 77 | repeated = Counter(repeated_hashes).most_common() 78 | reused = [] 79 | print('\n#### THIS IS SEPARATED AS :') 80 | for occurance in repeated: 81 | occurance = [x for x in occurance if x != 1] 82 | if len(occurance) != 1: 83 | print(*occurance, sep=":") 84 | reused.append(occurance) 85 | if len(reused) == 0: 86 | print("No repeated hashes!") 87 | else: 88 | print("No hashlist supplied, skipping") 89 | 90 | 91 | 92 | #Thanks Joshua Platz for his maskbuilder.py 93 | def masks(): 94 | print('\n\n\n###################### PASSWORD MASKS ######################\n') 95 | upper=["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"] 96 | lower=["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"] 97 | digit=["0","1","2","3","4","5","6","7","8","9"] 98 | masks={} 99 | words=0 100 | for word in plaintext_passwords: 101 | mask="" 102 | for char in word.rstrip('\r\n'): 103 | if char in upper: 104 | mask=mask+"?u" 105 | elif char in lower: 106 | mask=mask+"?l" 107 | elif char in digit: 108 | mask=mask+"?d" 109 | else: 110 | mask=mask+"?s" 111 | if mask not in masks: 112 | masks[mask] = 1 113 | else: 114 | masks[mask] += 1 115 | words+=1 116 | 117 | sorteddmask = sorted(masks.items(), key=operator.itemgetter(1), reverse=True) 118 | for mask in sorteddmask: 119 | if mask[1] >= 10: 120 | result = mask[0]+","+str(mask[1])+" Occurances,"+str((float(mask[1])/float(words))*100)+"%" 121 | print(result) 122 | 123 | 124 | # Gets the basewords from the plaintext passwords and counts the number of occurances 125 | def basewords_getter(): 126 | print('\n\n\n###################### COMMON BASEWORDS #########################\n') 127 | sorted_pws = [] 128 | for word in plaintext_passwords: 129 | baseword = (letters_regex.sub('',word.rstrip())) 130 | sorted_pws.append(baseword) 131 | repeated = Counter(sorted_pws).most_common() 132 | print('\n###### THIS IS SEPARATED AS :#####') 133 | for occurance in repeated: 134 | occurance = [x for x in occurance if x != 1] 135 | if len(occurance) != 1: 136 | print(*occurance, sep=":") 137 | 138 | # Looks for passwords without numbers or special characters 139 | def weak_passwords(): 140 | print("\n\n\n###################### WEAK PASSWORDS ######################\n") 141 | weak_pws = [] 142 | for word in plaintext_passwords: 143 | baseword = (letters_regex.sub('',word.rstrip())) 144 | if word.rstrip() == baseword: 145 | weak_pws.append(baseword) 146 | print('\n###### THESE ARE PASSWORDS THAT CONSIST OF ONLY LETTERS #####') 147 | weak_pws.sort() 148 | for occurance in weak_pws: 149 | print(occurance) 150 | 151 | def pw_length_check(): 152 | print("\n\n\n################### PASSWORD LENGTH CHECK ################\n") 153 | pw_length = [] 154 | for word in plaintext_passwords: 155 | length = len(word) 156 | pw_length.append(length) 157 | pw_length.sort(reverse = True) 158 | 159 | pw_count = Counter(pw_length) 160 | for pw_length, count in pw_count.most_common(): 161 | print(pw_length, ",", count) 162 | 163 | #Call the functions 164 | split_pot() 165 | basewords_getter() 166 | pw_reuse() 167 | masks() 168 | weak_passwords() 169 | pw_length_check() 170 | --------------------------------------------------------------------------------