├── .gitignore ├── LICENSE ├── README.md ├── google_api ├── ECCommutativeCipher.py ├── __init__.py ├── get_token.py ├── leak_detection_api_pb2.py └── proxy.py ├── img └── logo.png ├── kraken.py ├── requirements.txt ├── stats.py └── test_files ├── dictionary.txt ├── password.txt ├── regexs.txt └── users.txt /.gitignore: -------------------------------------------------------------------------------- 1 | refresh_token 2 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HN Security S.r.l. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Kraken

2 | 3 |

4 | kraken 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 | 16 | 17 | 18 |

19 | Generate a report with charts and statistics about cracked passwords in XLSX format 20 |

21 |

22 | Introduction 23 | • 24 | Authors 25 | • 26 | Features 27 | • 28 | Usage 29 |
30 |

31 | 32 |

33 | For more information check out this article 34 |

35 |
36 | 37 | 38 | ## Introduction 39 | During our assessments, it's common to get our hands on some hashed passwords, often related to an Active Directory (AD) domain. A password dump is usually obtained after compromising a domain controller and it contains the password hash of every account configured in the domain. In the Windows AD case, this hash is usually an NTLM hash, which doesn't offer strong protection against brute force and dictionary attacks. 40 | 41 | 42 |
43 | 44 | At this stage, we usually start a password cracking attack on the dumped hashes to assess the strength of the passwords used to access the domain's accounts. And guess what? Users' passwords are usually weak! 45 | 46 | This step allows us to get an overview of both the effectiveness of the password policies implemented in the domain and the awareness on the importance of strong passwords. We believe that this information is crucial to assess the security of an organization and, if presented correctly, it can have a significant impact on improving its security posture. However, the critical point is to present the results of this analysis in the best way possible. 47 | 48 | First of all, we need to avoid giving the chance of blaming the users or, even worse, finding a scapegoat to blame for the analysis results. The focus should always be on password policies and the importance of security awareness in the employees, never on single cases. For this reason, we anonymize the report, excluding any information that would allow identifying the users. 49 | 50 | Then, we need to show the impact of the analysis. Indeed, the list of cracked passwords doesn't provide a lot of information and it is not very useful by itself. For this reason, we need to aggregate the results and extract some useful statistics. 51 | 52 | To speed up and standardize this process we developed Kraken. Even if Kraken is designed to integrate with the workflow that we usually follow in this type of activity on Windows AD, it could also be used for similar activities that require password cracking (e.g. Azure AD, LDAP) with minor modifications. 53 | 54 | 55 | ## Authors 56 | - Maurizio Agazzini, Principal Security Analyst at HN Security   57 | - Stefano Alberto, Security Analyst at HN Security   58 | 59 | 60 | ## Features 61 | 62 | Kraken performs different types of analytics to visualize the results from different points of view, trying to identify patterns in the provided data. We will now take a brief look at the charts automatically generated by Kraken. 63 |

Password analysis

64 | This chart is pretty simple, yet crucial in understanding the impact of the analysis. It shows how many of the passwords we were able to crack, giving an immediate idea of the overall security posture. 65 |
66 |
67 | 68 |

69 | 70 |

71 | 72 |
73 |

Character analysis

74 | In this step we analyze the charset used in the cracked passwords. This is important to show potential flaws in the implemented password policy. If we can identify a prevalence of passwords that are based on small charsets we should probably consider implementing a more strict password policy. However, this chart should be interpreted carefully, knowing that it's biased because we are considering only the passwords that were successfully cracked. To address this bias, you can also include the not-cracked passwords in this chart by using the show-not-cracked option. 75 | 76 |
77 |
78 | 79 |

80 | 81 |

82 | 83 |
84 |

Length analysis

85 | This analysis is self explanatory, we want to check the length of the cracked passwords. This allows to evaluate the implemented password policy and identify possible flaws. As in the character analysis, we need to be aware that this chart is biased against the shorter passwords that are easier to crack. Also in this case, the show-not-cracked option can be used. 86 | 87 |
88 |
89 | 90 |

91 | 92 |

93 |
94 |

Topology analysis

95 | With this analysis, we try to highlight patterns in cracked passwords. The tool will automatically categorize some of the passwords based on some basic patterns. For example, we look for passwords that are based on the username or are composed of repeated words, adding more context to the chart. Typical examples are passwords that contain the name of the company or the year when the password was changed. We can simply add custom categories that will be used in this analysis with the dictionary and the regex options. 96 | 97 |
98 |
99 |
100 | 101 |

102 | 103 |

104 |
105 |
106 |

Leaked passwords analysis

107 | This is an optional analysis that checks automatically the presence of cracked credentials in public data breaches. It gives us an overview of the safety of passwords based on real data, allowing us to get another point of view on password strength. This check is done anonymously, the cleartext passwords never leave the device to guarantee password safety. We implemented this feature using two different services: 108 | 109 | haveibeenpwned 110 | 111 | This API offered by haveibeenpwned checks for the presence of the password in a database with hundreds of millions of real-world passwords previously exposed in data breaches. 112 | 113 | Google password manager 114 | 115 | This service checks for the user and password pair inside a database of leaked credentials. For more information about the design of this API, you can check out this blog post by Google. 116 | 117 | This service is freely available to Google Chrome users. However, the API specification is not publicly available. To overcome this limitation we reverse-engineered the protocol to implement it in our tool. 118 | 119 | Note that we need a token to use this API. To extract a valid token, the tool will spawn an instance of Google Chrome. You will need to be logged into the browser with a Google account to generate the token. We only use the token to emulate the requests generated by the browser to check the cracked passwords in the data breaches. However, we suggest using a secondary account for this purpose, to avoid any problems with your main Google account. To avoid launching the browser every time, the tool will save the token in the refresh_token file. If this file is found, the tool will check for the validity of the token, reusing it when possible. 120 | 121 |
122 |
123 | 124 |

125 | 126 |

127 |
128 |
129 |

Levenshtein distance analysis

130 | In a lot of cases, we have access to both the current and previous passwords of the users. When this happens, it's interesting to check if the users tend to reuse always the same password with small changes. This is pretty common when users are required to change often their password and it basically defeats the purpose of password rotation. Evaluating this phenomenon and visualizing it clearly will help us in understanding if the policies for password changing are effective. For this purpose, we need to define a metric to measure how much two passwords differ. We want a number that summarizes if two passwords are similar to each other. 131 | 132 | The Levenshtein distance is perfect for this purpose as it measures the "distance" between two strings. A low Levenshtein distance value means that the two strings are very similar to each other, while a high value means the opposite. To better understand this distance definition here are some examples: 133 | 134 |
135 |
136 | 137 |

138 | 139 |

140 |
141 |
Right now, we perform this analysis by computing the distance between the current and the last changed password, without considering the complete password history.
142 |
143 | 144 |

145 | 146 |

147 |
148 |

Common passwords

149 | Kraken also creates a ranking of the most common passwords in the cracked password list. This is useful to find more patterns in the password selection, for example, we can identify if a default password is used in the domain. This ranking could also be useful to extract a small set of passwords to add to the final report for the client when presenting the results. 150 |
151 |
152 | 153 |

154 | 155 |

156 |

Common hashes

157 | Similarly to the common password ranking, in this case, we rank the most common password hashes. This allows us to identify common passwords that we weren't able to crack that could add more information to the previous analysis. 158 | 159 |
160 |
161 |

162 | 163 |

164 |
165 | 166 | 167 | ## Usage 168 | 169 | Kraken is pretty straightforward to use, but, at the same time, it allows customizing the report case by case. 170 | 171 | In the most basic case, the tool takes the cracked password file as input and generates the report in the XLSX format. You can try it out with the following command. 172 |
173 |
python kraken.py --pwd passwords.txt --out report.xlsx
174 |
175 | The input file is expected in the format generated by hashcat, the current standard tool for password cracking. This format is pretty simple, every line contains a new set of credentials in the format "username:password". We also support the hexadecimal format used by hashcat to save passwords that contain special characters. In this case, the password is decoded automatically during the analysis. An example of a valid password file is the following: 176 |
username:password
177 | user2:p4ssw0rd
178 | anotheruser:$HEX[706124243a776f726421]
179 | Note that the tool also supports the process of historical password data. Previous passwords are identified by appending to the username the suffix _history and an incremental number. For example, this file contains the password history for the user username. 180 |
181 |
username:currentpassword
182 | username_history0:previouspassword
183 | username_history1:oldpassword
184 |
185 | In addition to the password file, the tool can take the following options as input. 186 |

Group

187 | This option is used to define a list of users to be considered in the analysis. The information provided with this option has two purposes: 188 | 191 | 194 | This allows the tool to consider in the statistics the non-cracked passwords, providing a better overview of the results. 195 | 196 | This option can be passed to the tool with the syntax --group user_file.txt where each line of the user file is a username that we want to consider in the analysis, as in the following example: 197 | 198 |
199 |
200 |
user
201 | user1
202 | user2
203 |
204 |

Show not cracked

205 | This option is used to include the information about the non-cracked passwords, obtained from the group option, in the generated statistics. With this option, we can choose if we want to focus the analysis only on the passwords that we were able to crack or if want to give a complete overview of the results. This choice is a matter of personal preference and it depends on the actual distribution of the data. Indeed, including this information would make the generated charts unusable if we cracked only a small portion of the passwords. However, if the portion of cracked passwords is significant adding this information will help give a better overview of the results. 206 | 207 | You can toggle this flag by adding the --show-not-cracked option to the command. 208 |

Dictionary

209 | As mentioned earlier, this option can be used to define new categories to be used in the password topology analysis. This is useful to customize the analysis and highlight some of the patterns that we identified in the cracked passwords. Indeed, it's pretty common to find out that a lot of passwords are based on some specific word. Usually, it is something related to the domain that we are testing, the most classic of examples is the name of the company. The tool will take each word in the dictionary file and it will use it to test the leaked passwords. 210 | 211 | To check if a password is based on a specific word, we consider it case insensitive and we also consider substitutions of letters and numbers (leet). For example, if we added the word "password" in the dictionary all of the following strings will match it and will be categorized based on the word "password": 212 | 217 | To use this option we can pass it to the tool with the syntax --dictionary dictionary_file. Each line of the dictionary file contains a word that we want to use to categorize passwords, as in the following example: 218 |
219 |
acme
220 | 2023
221 | password
222 |
223 |
224 |

Regex

225 | This option is similar to the dictionary option, but it allows us to be more flexible in the categorization of the passwords. 226 | 227 | For example, if we want to highlight the passwords containing a date we could use a regex similar to .*\d{8}.*. This regex would match all of the following passwords: 228 | 229 |
230 |
231 |
02121999
232 | test06072022
233 | tt00000000!
234 |
235 |
236 | 237 | To use this option we can pass it to the tool with the syntax --regex regex_file.txt. Each line of the regex file contains a python regex that will be used to match the cracked passwords, as in the following example: 238 |
239 |
p(a|4)s.+rd
240 | .*\d{6}.*
241 |
242 |

Pwdump file

243 | Kraken can also take as input the raw hash dump that we used in hashcat to crack the passwords. This file adds some information on all the passwords, even the ones that we weren't able to crack. At the moment this file is used only to generate the ranking of the most common password hashes available in the dump. 244 | 245 | You can pass this option with the syntax pwdump hash_dump_file.txt. The file format expected by Kraken is the output format of secretsdump.py. An example of this file format is the following: 246 |
247 |
octagon.local\Administrator:500:aad3b435b51404eeaad3b435b51404ee:c8ca0f8d1f3ca975464bee8843bceda3:::
248 | Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
249 | krbtgt:502:aad3b435b51404eeaad3b435b51404ee:52db83a2f77be50bdb0f9cad2978d682:::
250 | octagon.local\REVA_JOYCE:1103:aad3b435b51404eeaad3b435b51404ee:b2102052a134db43cdf57649d36aecbb:::
251 | octagon.local\MARYANN_HANSON:1104:aad3b435b51404eeaad3b435b51404ee:57cbd01ad63e05402db21fb22cdedda2:::
252 | octagon.local\MICHELLE_WOLF:1105:aad3b435b51404eeaad3b435b51404ee:65db1dfba7ef28bd9a95c2b4c0a8c213:::
253 |
254 |

Check leaked

255 | With this option, you can enable the feature that checks the presence of cracked credentials in public data breaches. As described earlier, this feature supports two different APIs. 256 | 257 | You can enable this analysis using the haveibeenpwned API by adding the flag --check-leaked in the command. 258 | 259 | If you also what to enable the API used by Google password manager you can add the --check-leaked-google flag too. 260 | -------------------------------------------------------------------------------- /google_api/ECCommutativeCipher.py: -------------------------------------------------------------------------------- 1 | 2 | from ecpy.curves import Curve, Point 3 | from math import ceil 4 | from hashlib import sha256 5 | from Crypto.Util.number import inverse 6 | from libnum import sqrtmod_prime_power 7 | import random 8 | 9 | 10 | class ECCommutativeCipher: 11 | 12 | cv = Curve.get_curve("NIST-P256") 13 | 14 | p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff 15 | a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc 16 | b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b 17 | order = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 18 | 19 | def __init__(self, private_key=None): 20 | if (private_key is None): 21 | self.key = random.randint(1, self.order) 22 | else: 23 | self.key = int.from_bytes(private_key, 'big') 24 | 25 | def random_oracle(self, x, max_value): 26 | 27 | hash_output_length = 256 28 | output_bit_length = max_value.bit_length() + hash_output_length 29 | iter_count = ceil(output_bit_length / hash_output_length) 30 | assert (iter_count * hash_output_length < 130048) 31 | excess_bit_count = ( 32 | iter_count * hash_output_length) - output_bit_length 33 | hash_output = 0 34 | 35 | for i in range(1, iter_count+1): 36 | hash_output = hash_output << hash_output_length 37 | bignum_bytes = i.to_bytes(ceil(i.bit_length()/8), 'big') 38 | bignum_bytes += x 39 | # print(' bytes: ', bignum_bytes.hex()) 40 | hashed_string = sha256(bignum_bytes).digest() 41 | hash_output = hash_output + int.from_bytes(hashed_string, 'big') 42 | #print(' ', hash_output) 43 | return (hash_output >> excess_bit_count) % max_value 44 | 45 | def hashToTheCurve(self, m): 46 | m = m.split(b'\x00')[0] 47 | x = self.random_oracle(m, self.p) 48 | while True: 49 | # print('x: ', x) 50 | mod_x = x % self.p 51 | y2 = (mod_x**3 + self.a*mod_x + self.b) % self.p 52 | try: 53 | sqrt = list(sqrtmod_prime_power(y2, self.p, 1))[0] 54 | if (sqrt & 1 == 1): 55 | return Point(mod_x, (-sqrt) % self.p, self.cv) 56 | return Point(mod_x, sqrt, self.cv) 57 | except: 58 | pass 59 | x = self.random_oracle(x.to_bytes( 60 | ceil(x.bit_length()/8), 'big'), self.p) 61 | 62 | def encrypt(self, plaintext): 63 | point = self.hashToTheCurve(plaintext) 64 | ep = self.key*point 65 | 66 | ser_x = ep.x.to_bytes(32, 'big') 67 | ser_y = ep.y.to_bytes(32, 'big') 68 | return bytes([2 + (ser_y[-1] & 1)]) + ser_x 69 | 70 | def decrypt(self, ciphertext): 71 | assert (ciphertext[0] == 2 or ciphertext[0] == 3) 72 | 73 | x = int.from_bytes(ciphertext[1:], 'big') 74 | y = self.cv.y_recover(x) 75 | 76 | point = Point(x, y, self.cv) 77 | 78 | dp = inverse(self.key, self.order) * point 79 | 80 | ser_x = dp.x.to_bytes(32, 'big') 81 | ser_y = dp.y.to_bytes(32, 'big') 82 | 83 | return bytes([2 + (ser_y[-1] & 1)]) + ser_x 84 | 85 | 86 | if (__name__ == '__main__'): 87 | 88 | x = b'\x39\x4f\xf8\x31\x41\xc4\xaf\x41\xbd\x3b\x5e\xf9\x1d\xeb\x72\x9d\xab\x98\x9e\x72\x31\xfe\xd8\x20\xd2\x22\xb3\xbc\xec\x89\x14\x8e' 89 | 90 | cipher = ECCommutativeCipher() 91 | 92 | c = cipher.encrypt(x) 93 | 94 | print(c.hex()) 95 | 96 | with open('reencrypted_lookup_hash', 'rb') as f: 97 | reencrypted_lookup_hash = f.read() 98 | 99 | d = cipher.decrypt(reencrypted_lookup_hash) 100 | 101 | print(d.hex()) 102 | -------------------------------------------------------------------------------- /google_api/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from hashlib import sha256, scrypt 3 | import google_api.leak_detection_api_pb2 as LookupSingleLeakRequest 4 | from google_api.get_token import get_token 5 | from google_api.ECCommutativeCipher import ECCommutativeCipher 6 | 7 | 8 | def username_hash_prefix(username): 9 | username_salt = b'\xC4\x94\xA3\x95\xF8\xC0\xE2\x3E\xA9\x23\x04\x78\x70\x2C\x72\x18\x56\x54\x99\xB3\xE9\x21\x18\x6C\x21\x1A\x01\x22\x3C\x45\x4A\xFA' 10 | hash = sha256(username.encode()+username_salt).hexdigest()[:8] 11 | x = bytes.fromhex(hash) 12 | x = x[:3] + bytes([x[3] & 0b11000000]) 13 | return x 14 | 15 | 16 | def scrypt_hash_username_and_password(username, password): 17 | password_salt = b'\x30\x76\x2A\xD2\x3F\x7B\xA1\x9B\xF8\xE3\x42\xFC\xA1\xA7\x8D\x06\xE6\x6B\xE4\xDB\xB8\x4F\x81\x53\xC5\x03\xC8\xDB\xBd\xDE\xA5\x20' 18 | username_password = username.encode() + password.encode() 19 | salt = username.encode() + password_salt 20 | hash = scrypt(username_password, salt=salt, n=4096, r=8, p=1)[:32] 21 | return hash 22 | 23 | 24 | class GoogleApi(): 25 | 26 | def __init__(self): 27 | self.access_token = get_token() 28 | self.cipher = ECCommutativeCipher() 29 | 30 | def lookup_request(self, username, password): 31 | lookup_hash = scrypt_hash_username_and_password(username, password) 32 | 33 | req = LookupSingleLeakRequest.LookupSingleLeakRequest() 34 | 35 | req.username_hash_prefix = username_hash_prefix(username) 36 | req.username_hash_prefix_length = 26 37 | req.encrypted_lookup_hash = self.cipher.encrypt(lookup_hash) 38 | 39 | serialized = req.SerializeToString() 40 | 41 | r = requests.post( 42 | 'https://passwordsleakcheck-pa.googleapis.com/v1/leaks:lookupSingle', 43 | headers={'authorization': 'Bearer ' + self.access_token, 'content-type': 'application/x-protobuf', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'no-cors', 'sec-fetch-dest': 'empty', 44 | 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'}, 45 | data=serialized) 46 | 47 | if (r.status_code != 200): 48 | print(r) 49 | print(r.text) 50 | raise "Request failed" 51 | 52 | res = LookupSingleLeakRequest.LookupSingleLeakResponse() 53 | res.ParseFromString(r.content) 54 | 55 | return res 56 | 57 | def is_leaked(self, username, password): 58 | res = self.lookup_request(username, password) 59 | 60 | dec = self.cipher.decrypt(res.reencrypted_lookup_hash) 61 | 62 | hash1 = sha256(b'\x02' + dec[1:]).digest() 63 | hash2 = sha256(b'\x03' + dec[1:]).digest() 64 | 65 | for x in res.encrypted_leak_match_prefix: 66 | if (hash1.startswith(x) or hash2.startswith(x)): 67 | return True 68 | 69 | return False 70 | -------------------------------------------------------------------------------- /google_api/get_token.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import subprocess 3 | from google_api.proxy import getTokenFromChrome 4 | 5 | 6 | def refresh_token(old_token): 7 | r = requests.post('https://www.googleapis.com/oauth2/v4/token', headers={'sec-fetch-site': 'none', 'sec-fetch-mode': 'no-cors', 'sec-fetch-dest': 'empty', 8 | 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'}, 9 | data={"scope": "https://www.googleapis.com/auth/identity.passwords.leak.check", 10 | "grant_type": "refresh_token", 11 | "refresh_token": old_token, 12 | "client_id": "77185425430.apps.googleusercontent.com", 13 | "client_secret": "OTJgUOQcT7lO7GsGZq2G4IlT"}) 14 | 15 | return r.json()['access_token'] 16 | 17 | 18 | def get_token(): 19 | try: 20 | with open('refresh_token', 'r') as f: 21 | rt = f.read() 22 | return refresh_token(rt) 23 | except: 24 | rt = getTokenFromChrome() 25 | 26 | with open('refresh_token', 'w') as f: 27 | f.write(rt) 28 | 29 | return refresh_token(rt) 30 | -------------------------------------------------------------------------------- /google_api/leak_detection_api_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: leak_detection_api.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf.internal import builder as _builder 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( 15 | b'\n\x18leak_detection_api.proto\x12\x30google.internal.identity.passwords.leak.check.v1\"{\n\x17LookupSingleLeakRequest\x12\x1c\n\x14username_hash_prefix\x18\x01 \x01(\x0c\x12#\n\x1busername_hash_prefix_length\x18\x02 \x01(\r\x12\x1d\n\x15\x65ncrypted_lookup_hash\x18\x03 \x01(\x0c\"`\n\x18LookupSingleLeakResponse\x12#\n\x1b\x65ncrypted_leak_match_prefix\x18\x01 \x03(\x0c\x12\x1f\n\x17reencrypted_lookup_hash\x18\x02 \x01(\x0c\x42\x02H\x03\x62\x06proto3') 16 | 17 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 18 | _builder.BuildTopDescriptorsAndMessages( 19 | DESCRIPTOR, 'leak_detection_api_pb2', globals()) 20 | if _descriptor._USE_C_DESCRIPTORS == False: 21 | 22 | DESCRIPTOR._options = None 23 | DESCRIPTOR._serialized_options = b'H\003' 24 | _LOOKUPSINGLELEAKREQUEST._serialized_start = 78 25 | _LOOKUPSINGLELEAKREQUEST._serialized_end = 201 26 | _LOOKUPSINGLELEAKRESPONSE._serialized_start = 203 27 | _LOOKUPSINGLELEAKRESPONSE._serialized_end = 299 28 | # @@protoc_insertion_point(module_scope) 29 | -------------------------------------------------------------------------------- /google_api/proxy.py: -------------------------------------------------------------------------------- 1 | from mitm import MITM, protocol, crypto, middleware 2 | from base64 import b64encode 3 | import subprocess 4 | from hashlib import sha256 5 | import OpenSSL 6 | from mitm.core import Connection, Middleware 7 | import re 8 | 9 | 10 | def getTokenFromChrome(): 11 | 12 | while True: 13 | ans = input('''Using the reverse engineered Google api requires a valid token. 14 | We need to launch Google Chrome to extract a token from your browser, the browser will be closed as soon as we get the token. 15 | Type Y to continue or N to exit: ''') 16 | if ans.lower() == 'y': 17 | break 18 | if ans.lower() == 'n': 19 | exit(1) 20 | 21 | ca = crypto.CertificateAuthority() 22 | 23 | # pk = ca.cert.get_pubkey() 24 | # der = OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_ASN1, pk) 25 | # fingerprint = b64encode(sha256(der).digest()).decode() 26 | 27 | mitm_server = None 28 | 29 | class GetRefreshToken(Middleware): 30 | def __init__(self): 31 | self.connection: Connection = None 32 | 33 | async def mitm_started(self, host: str, port: int): 34 | # print(f"MITM server started on {host}:{port}.") 35 | pass 36 | 37 | async def client_connected(self, connection: Connection): 38 | pass 39 | 40 | async def server_connected(self, connection: Connection): 41 | pass 42 | 43 | async def client_data(self, connection: Connection, data: bytes) -> bytes: 44 | m = re.search(b'refresh_token=([^&]+)&', data) 45 | if m: 46 | global refresh_token 47 | refresh_token = m[1].decode() 48 | mitm_server.stop() 49 | 50 | return data 51 | 52 | async def server_data(self, connection: Connection, data: bytes) -> bytes: 53 | return data 54 | 55 | async def client_disconnected(self, connection: Connection): 56 | pass 57 | 58 | async def server_disconnected(self, connection: Connection): 59 | pass 60 | 61 | mitm = MITM( 62 | host="127.0.0.1", 63 | port=8899, 64 | protocols=[protocol.HTTP], 65 | middlewares=[GetRefreshToken], 66 | certificate_authority=ca 67 | ) 68 | 69 | p = subprocess.Popen( 70 | ['google-chrome', '--proxy-server=localhost:8899', '--ignore-certificate-errors', 'https://account.google.com'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 71 | 72 | mitm_server = mitm 73 | 74 | try: 75 | mitm.run() 76 | except: 77 | pass 78 | 79 | p.kill() 80 | return refresh_token 81 | 82 | 83 | if __name__ == '__main__': 84 | print(getTokenFromChrome()) 85 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hnsecurity/kraken/6cc4f460e7b4da99b133744bc795bb75fce2df6d/img/logo.png -------------------------------------------------------------------------------- /kraken.py: -------------------------------------------------------------------------------- 1 | from stats import levenshtein_distance_stats 2 | from openpyxl.styles import Font 3 | import re 4 | import collections 5 | import openpyxl 6 | import argparse 7 | from stats import * 8 | import logging 9 | import traceback 10 | 11 | parser = argparse.ArgumentParser(description='Password stats generator') 12 | 13 | parser.add_argument('--pwd', dest='filename_password', 14 | help='pasword file', required=True) 15 | parser.add_argument('--out', dest='out_file', 16 | help='output file', required=True) 17 | parser.add_argument('--group', dest='filename_users', 18 | help='group file', required=False) 19 | parser.add_argument('--regex', dest='regex_file', 20 | help='regex file', required=False) 21 | parser.add_argument('--dictionary', dest='dictionary_file', 22 | help='dictionary file', required=False) 23 | 24 | parser.add_argument('--debug', dest='debug', 25 | help='enable debug logs', action='store_true') 26 | parser.add_argument('--show-not-cracked', dest='show_not_cracked', 27 | help='enable not cracked password into the XLSX', action='store_true', required=False) 28 | parser.add_argument('--ignore-case', dest='ignore_case', 29 | help='ignore case change in Levenshtein distance', action='store_true', required=False) 30 | parser.add_argument('--pwdump', dest='pwdump_file', 31 | help='enable hash count', required=False) 32 | 33 | parser.add_argument('--check-leaked', dest='check_leaked', 34 | help='check if leaked', action='store_true') 35 | parser.add_argument('--check-leaked-google', dest='check_leaked_google', 36 | help='check leaked username,password pair in google database', action='store_true') 37 | 38 | args = parser.parse_args() 39 | 40 | dictionary_list = [] 41 | list_user_password = {} 42 | list_user_password_with_history = {} 43 | list_users = None 44 | list_regex = [] 45 | list_user_hash = {} 46 | 47 | 48 | logger = logging.getLogger('logger') 49 | 50 | if args.debug: 51 | logger.setLevel(logging.DEBUG) 52 | logger.info('Debug logs enabled') 53 | 54 | # load user list 55 | try: 56 | if args.filename_users: 57 | with open(args.filename_users) as f: 58 | list_users = f.read().splitlines() 59 | logger.info(f'Loaded {len(list_users)} users') 60 | logger.debug('Loaded users: ' + str(list_users)) 61 | except: 62 | logger.warning( 63 | 'Error while reading users file, skipping...') 64 | list_users = None 65 | 66 | # load regex 67 | try: 68 | if args.regex_file: 69 | with open(args.regex_file) as f: 70 | list_regex = f.read().splitlines() 71 | logger.info(f'Loaded {len(list_regex)} regexs') 72 | logger.debug('Loaded regexs: ' + str(list_regex)) 73 | except: 74 | logger.warning('Error while reading regex file, skipping...') 75 | list_regex = [] 76 | 77 | # load dictionary 78 | try: 79 | if args.dictionary_file: 80 | with open(args.dictionary_file) as f: 81 | for cur_line in f.readlines(): 82 | cur_line = cur_line.rstrip() 83 | dictionary_list.append(cur_line) 84 | logger.info(f'Loaded {len(dictionary_list)} words from dictionary') 85 | logger.debug('Loaded words: ' + str(dictionary_list)) 86 | except: 87 | logger.warning('Error while reading dictionary file, skipping...') 88 | dictionary_list = [] 89 | 90 | # load only password in user list 91 | n_passwords = 0 92 | 93 | try: 94 | with open(args.filename_password) as f: 95 | for cur_line in f: 96 | 97 | try: 98 | cur_user, cur_password = cur_line.split(':', 1) 99 | 100 | # decode the $HEX[] hashcat format 101 | m = re.search(r'^\$HEX\[([0-9a-f]+)\]$',cur_password) 102 | if m: 103 | cur_password = bytes.fromhex(m[1]).decode() 104 | except: 105 | continue 106 | 107 | # clean user from history 108 | cur_user_clean = re.sub("_history[0-9]*$", "", cur_user) 109 | 110 | # insert in list only present users 111 | if not list_users or cur_user_clean in list_users: 112 | n_passwords += 1 113 | if "_history" in cur_user: 114 | list_user_password_with_history[cur_user] = cur_password.rstrip( 115 | '\r\n') 116 | else: 117 | list_user_password[cur_user] = cur_password.rstrip('\r\n') 118 | except: 119 | logger.critical('Error while reading password file, exiting...') 120 | logger.debug(traceback.format_exc()) 121 | exit(1) 122 | 123 | logger.info(f'Loaded {n_passwords} passwords') 124 | 125 | # load only NTHASH in user list 126 | if args.pwdump_file: 127 | try: 128 | n_hashes = 0 129 | with open(args.pwdump_file) as f: 130 | for cur_line in f: 131 | 132 | try: 133 | cur_user, a, b, cur_password, c = cur_line.split(':', 4) 134 | except: 135 | continue 136 | 137 | # clean user from history 138 | cur_user_clean = re.sub("_history[0-9]*$", "", cur_user) 139 | 140 | # insert in list only present users 141 | if not list_users or cur_user_clean in list_users: 142 | 143 | list_user_hash[cur_user] = cur_password.rstrip('\r\n') 144 | n_hashes += 1 145 | 146 | logger.info(f'Loaded {n_hashes} hashes') 147 | except: 148 | logger.warning('Error while reading pwdump file, skipping...') 149 | 150 | 151 | # create the excel workbook 152 | wb = openpyxl.Workbook() 153 | 154 | ws_stats = wb.create_sheet('Stats') 155 | ws_hash = wb.create_sheet('Hash') 156 | ws_pwd = wb.create_sheet('Most used') 157 | 158 | # remove the default sheet 159 | wb.remove(wb['Sheet']) 160 | 161 | ws_stats.column_dimensions['B'].width = 20 162 | 163 | 164 | # statistics 165 | cur_line = 2 166 | 167 | if not list_users is None: 168 | logger.debug('Starting password analysis...') 169 | cur_line = password_analysis_stats(ws_stats, cur_line, args, list_users, 170 | list_user_password) 171 | cur_line += 5 172 | 173 | logger.debug('Starting characters analysis...') 174 | cur_line = characters_analysis_stats(ws_stats, cur_line, args, list_users, 175 | list_user_password) 176 | 177 | logger.debug('Starting password length analysis...') 178 | cur_line = password_length_stats(ws_stats, cur_line + 5, args, list_users, 179 | list_user_password) 180 | 181 | logger.debug('Starting password topology analysis...') 182 | cur_line = password_topology_stats( 183 | ws_stats, cur_line + 5, args, list_users, list_user_password, list_regex, dictionary_list) 184 | 185 | if args.check_leaked: 186 | logger.info('Starting leaked password analysis, this could take some time...') 187 | cur_line = password_leaked_stats( 188 | ws_stats, cur_line + 5, args, list_users, list_user_password, list_regex, dictionary_list) 189 | else: 190 | logger.debug('Skipping leaked password analysis...') 191 | 192 | if len(list_user_password_with_history) != 0: 193 | logger.debug('Starting levenshtein distance analysis...') 194 | cur_line = levenshtein_distance_stats(ws_stats, cur_line + 5, args, list_users, 195 | list_user_password, list_user_password_with_history) 196 | else: 197 | logger.debug('Skipping levenshtein distance analysis...') 198 | 199 | # Password occurences 200 | ws_pwd["A1"] = "Password" 201 | ws_pwd["B1"] = "Occurences" 202 | 203 | cur_line = 2 204 | 205 | c = collections.Counter(list_user_password[elem] 206 | for elem in list_user_password) 207 | 208 | for i in c.most_common(): 209 | if i[1] > 3: 210 | ws_pwd["A" + str(cur_line)] = i[0] 211 | ws_pwd["B" + str(cur_line)] = i[1] 212 | cur_line += 1 213 | 214 | # Hash occurences 215 | ws_hash["A1"] = "Hash" 216 | ws_hash["B1"] = "Occurences" 217 | 218 | cur_line = 2 219 | 220 | c = collections.Counter(list_user_hash[elem] for elem in list_user_hash) 221 | 222 | for i in c.most_common(): 223 | if i[1] > 3: 224 | ws_hash["A" + str(cur_line)] = i[0] 225 | ws_hash["B" + str(cur_line)] = i[1] 226 | cur_line += 1 227 | 228 | 229 | logger.info('Saving output file') 230 | wb.save(args.out_file) 231 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs 2 | certifi 3 | cffi 4 | charset-normalizer 5 | cryptography 6 | ECPy 7 | et-xmlfile 8 | future 9 | httpq 10 | idna 11 | Levenshtein 12 | libnum 13 | mitm 14 | openpyxl 15 | pbr 16 | protobuf 17 | pwnedpasswords 18 | pycparser 19 | pycryptodome 20 | pyOpenSSL 21 | rapidfuzz 22 | requests 23 | toolbox 24 | tqdm 25 | urllib3 26 | -------------------------------------------------------------------------------- /stats.py: -------------------------------------------------------------------------------- 1 | from tqdm import tqdm 2 | import re 3 | from openpyxl.chart import PieChart3D, PieChart, ProjectedPieChart, BarChart, Reference, Series 4 | from openpyxl.styles import Font 5 | import operator 6 | from Levenshtein import distance 7 | import logging 8 | 9 | from google_api import GoogleApi 10 | import pwnedpasswords 11 | 12 | logger = logging.getLogger('logger') 13 | 14 | 15 | def password_analysis_stats(ws_stats, start_line, args, list_users, list_user_password): 16 | ws_stats[f"B{start_line}"] = "Passwords Analysis" 17 | ws_stats[f"B{start_line}"].font = Font(bold=True) 18 | 19 | ws_stats[f"B{start_line+1}"] = "Analyzed" 20 | ws_stats[f"C{start_line+1}"] = len(list_user_password) 21 | 22 | ws_stats[f"B{start_line+2}"] = "Not analyzed*" 23 | 24 | if list_users: 25 | ws_stats[f"C{start_line+2}"] = len(list_users) - \ 26 | len(list_user_password) 27 | else: 28 | ws_stats[f"C{start_line+2}"] = 0 29 | 30 | chart = PieChart3D() 31 | chart.add_data( 32 | Reference(ws_stats, range_string=f'Stats!C{start_line+1}:C{start_line+2}')) 33 | chart.set_categories( 34 | Reference(ws_stats, range_string=f'Stats!B{start_line+1}:B{start_line+2}')) 35 | chart.title = "Password Analysis" 36 | ws_stats.add_chart(chart, f'E{start_line}') 37 | 38 | return start_line + 15 39 | 40 | 41 | def characters_analysis_stats(ws_stats, start_line, args, list_users, list_user_password): 42 | password_chars = {} 43 | password_chars["Uppercase"] = 0 44 | password_chars["Lowercase"] = 0 45 | password_chars["Digits"] = 0 46 | password_chars["Specials"] = 0 47 | password_chars["Digits Lowercase"] = 0 48 | password_chars["Digits Uppercase"] = 0 49 | password_chars["Digits Specials"] = 0 50 | password_chars["Lowercase Uppercase"] = 0 51 | password_chars["Lowercase Specials"] = 0 52 | password_chars["Uppercase Specials"] = 0 53 | password_chars["Digits Lowercase Uppercase"] = 0 54 | password_chars["Digits Lowercase Specials"] = 0 55 | password_chars["Digits Uppercase Specials"] = 0 56 | password_chars["Lowercase Uppercase Specials"] = 0 57 | password_chars["Digits Lowercase Uppercase Specials"] = 0 58 | 59 | for cur_user in list_user_password: 60 | 61 | # don't count on password history 62 | if re.search(r"_history[0-9]*$", cur_user): 63 | continue 64 | 65 | # check password chars 66 | if re.search(r"(?=^[^\x0d\x0a]*$)(?!.*\d)(?=.*[A-Z])(?!.*[a-z])(?!.*[\W])", list_user_password[cur_user]): 67 | password_chars["Uppercase"] += 1 68 | continue 69 | if re.search(r"(?=^[^\x0d\x0a]*$)(?!.*\d)(?!.*[A-Z])(?=.*[a-z])(?!.*[\W])", list_user_password[cur_user]): 70 | password_chars["Lowercase"] += 1 71 | continue 72 | if re.search(r"(?=^[^\x0d\x0a]*$)(?=.*\d)(?!.*[A-Z])(?!.*[a-z])(?!.*[\W])", list_user_password[cur_user]): 73 | password_chars["Digits"] += 1 74 | continue 75 | if re.search(r"(?=^[^\x0d\x0a]*$)(?!.*\d)(?!.*[A-Z])(?!.*[a-z])(?=.*[\W])", list_user_password[cur_user]): 76 | password_chars["Specials"] += 1 77 | continue 78 | if re.search(r"(?=^[^\x0d\x0a]*$)(?=.*\d)(?!.*[A-Z])(?=.*[a-z])(?!.*[\W])", list_user_password[cur_user]): 79 | password_chars["Digits Lowercase"] += 1 80 | continue 81 | if re.search(r"(?=^[^\x0d\x0a]*$)(?=.*\d)(?=.*[A-Z])(?!.*[a-z])(?!.*[\W])", list_user_password[cur_user]): 82 | password_chars["Digits Uppercase"] += 1 83 | continue 84 | if re.search(r"(?=^[^\x0d\x0a]*$)(?=.*\d)(?!.*[A-Z])(?!.*[a-z])(?=.*[\W])", list_user_password[cur_user]): 85 | password_chars["Digits Specials"] += 1 86 | continue 87 | if re.search(r"(?=^[^\x0d\x0a]*$)(?!.*\d)(?=.*[A-Z])(?=.*[a-z])(?!.*[\W])", list_user_password[cur_user]): 88 | password_chars["Lowercase Uppercase"] += 1 89 | continue 90 | if re.search(r"(?=^[^\x0d\x0a]*$)(?!.*\d)(?!.*[A-Z])(?=.*[a-z])((?=.*[\W])|(?=.*[\s]))", list_user_password[cur_user]): 91 | password_chars["Lowercase Specials"] += 1 92 | continue 93 | if re.search(r"(?=^[^\x0d\x0a]*$)(?!.*\d)(?=.*[A-Z])(?!.*[a-z])(?=.*[\W])", list_user_password[cur_user]): 94 | password_chars["Uppercase Specials"] += 1 95 | continue 96 | if re.search(r"(?=^[^\x0d\x0a]*$)(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?!.*[\W])", list_user_password[cur_user]): 97 | password_chars["Digits Lowercase Uppercase"] += 1 98 | continue 99 | if re.search(r"(?=^[^\x0d\x0a]*$)(?=.*\d)(?!.*[A-Z])(?=.*[a-z])(?=.*[\W])", list_user_password[cur_user]): 100 | password_chars["Digits Lowercase Specials"] += 1 101 | continue 102 | if re.search(r"(?=^[^\x0d\x0a]*$)(?=.*\d)(?=.*[A-Z])(?!.*[a-z])(?=.*[\W])", list_user_password[cur_user]): 103 | password_chars["Digits Uppercase Specials"] += 1 104 | continue 105 | if re.search(r"(?=^[^\x0d\x0a]*$)(?!.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[\W])", list_user_password[cur_user]): 106 | password_chars["Lowercase Uppercase Specials"] += 1 107 | continue 108 | if re.search(r"(?=^[^\x0d\x0a]*$)(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[\W])", list_user_password[cur_user]): 109 | password_chars["Digits Lowercase Uppercase Specials"] += 1 110 | continue 111 | 112 | ws_stats[f"B{start_line}"] = "Characters Analysis" 113 | ws_stats[f"B{start_line}"].font = Font(bold=True) 114 | 115 | cur_line = start_line + 1 116 | 117 | if args.show_not_cracked and list_users: 118 | ws_stats["B" + str(cur_line)] = "Not cracked" 119 | ws_stats["C" + str(cur_line)] = len(list_users) - \ 120 | len(list_user_password) 121 | cur_line += 1 122 | 123 | for i in sorted(password_chars.items(), key=operator.itemgetter(1), reverse=True): 124 | if i[1] > 0: 125 | ws_stats["B" + str(cur_line)] = i[0] 126 | ws_stats["C" + str(cur_line)] = i[1] 127 | cur_line += 1 128 | 129 | chart = ProjectedPieChart() 130 | chart.add_data( 131 | Reference(ws_stats, range_string=f'Stats!C{start_line+1}:C{cur_line-1}')) 132 | chart.set_categories( 133 | Reference(ws_stats, range_string=f'Stats!B{start_line+1}:B{cur_line-1}')) 134 | chart.title = "Characters Analysis" 135 | ws_stats.add_chart(chart, f'E{start_line}') 136 | 137 | return max(cur_line, start_line+15) 138 | 139 | 140 | def password_length_stats(ws_stats, start_line, args, list_users, list_user_password): 141 | cut_length = 14 142 | 143 | password_len = {} 144 | for i in range(0, cut_length+2): 145 | password_len[i] = 0 146 | 147 | for cur_user in list_user_password: 148 | # compute len of password 149 | if len(list_user_password[cur_user]) > cut_length: 150 | password_len[cut_length+1] += 1 151 | else: 152 | password_len[len(list_user_password[cur_user])] += 1 153 | 154 | ws_stats[f"B{start_line}"] = "Password Length" 155 | ws_stats[f"B{start_line}"].font = Font(bold=True) 156 | 157 | cur_line = start_line + 1 158 | 159 | if args.show_not_cracked and list_users: 160 | ws_stats["B" + str(cur_line)] = "NC" 161 | ws_stats["C" + str(cur_line)] = len(list_users) - \ 162 | len(list_user_password) 163 | cur_line += 1 164 | 165 | max_length = max([i for i in range(cut_length+1) if password_len[i] != 0]+[0]) 166 | if password_len[cut_length+1] != 0: 167 | max_length = cut_length 168 | 169 | for i in range(0, max_length+1): 170 | ws_stats["B" + str(cur_line)] = i 171 | ws_stats["C" + str(cur_line)] = password_len[i] 172 | cur_line += 1 173 | 174 | if password_len[cut_length+1] != 0: 175 | ws_stats["B" + str(cur_line)] = f">{cut_length}" 176 | ws_stats["C" + str(cur_line)] = password_len[cut_length+1] 177 | cur_line += 1 178 | 179 | chart = BarChart() 180 | chart.add_data( 181 | Reference(ws_stats, range_string=f'Stats!C{start_line+1}:C{cur_line-1}')) 182 | chart.set_categories( 183 | Reference(ws_stats, range_string=f'Stats!B{start_line+1}:B{cur_line-1}')) 184 | chart.title = "Password length" 185 | chart.legend = None 186 | ws_stats.add_chart(chart, f'E{start_line}') 187 | 188 | return max(cur_line, start_line+15) 189 | 190 | 191 | def convert_into_l33t_regex(s): 192 | n = "" 193 | 194 | for c in s: 195 | n = n + '[' + str(c) + c.swapcase() 196 | 197 | if c == 'a': 198 | n = n + '4' 199 | elif c == 'e': 200 | n = n + '3' 201 | elif c == 'i': 202 | n = n + '1' 203 | elif c == 'o': 204 | n = n + '0' 205 | 206 | n = n + ']+' 207 | 208 | return n 209 | 210 | def password_leaked_stats(ws_stats, start_line, args, list_users, list_user_password, list_regex, dictionary_list): 211 | leaked_passwords_google = 0 212 | leaked_passwords_HIBP = 0 213 | count_leaked = 0 214 | count_not_leaked = 0 215 | 216 | if args.check_leaked_google: 217 | logger.debug('Getting token to use the google api (leaked passwords)') 218 | google_leak_api = GoogleApi() 219 | logger.debug('Google token found') 220 | 221 | leaked_set_HIBP = set() 222 | leaked_set_google = set() 223 | 224 | for cur_user in tqdm(list_user_password): 225 | # don't count on password history 226 | if re.search(r"_history[0-9]*$", cur_user): 227 | continue 228 | 229 | leaked = False 230 | # check if password was leaked 231 | 232 | if args.check_leaked_google: 233 | # clean the username 234 | cleaned_username = cur_user.split('\\')[-1] 235 | if google_leak_api.is_leaked(cleaned_username, list_user_password[cur_user]): 236 | leaked_passwords_google += 1 237 | leaked_set_google.add((cleaned_username,list_user_password[cur_user])) 238 | leaked = True 239 | 240 | if pwnedpasswords.check(list_user_password[cur_user]): 241 | leaked_passwords_HIBP += 1 242 | leaked_set_HIBP.add(list_user_password[cur_user]) 243 | leaked = True 244 | 245 | if not leaked: 246 | count_not_leaked += 1 247 | else: 248 | count_leaked += 1 249 | 250 | logger.debug('Leaked passwords HIBP: ' + str(leaked_set_HIBP)) 251 | logger.debug('Leaked passwords Google: ' + str(leaked_set_google)) 252 | 253 | ws_stats[f"B{start_line}"] = "Leaked Password" 254 | ws_stats[f"B{start_line}"].font = Font(bold=True) 255 | 256 | cur_line = start_line+1 257 | 258 | ws_stats["B" + str(cur_line)] = 'Leaked' 259 | ws_stats["C" + str(cur_line)] = count_leaked 260 | cur_line += 1 261 | 262 | ws_stats["B" + str(cur_line)] = 'Not leaked' 263 | ws_stats["C" + str(cur_line)] = count_not_leaked 264 | cur_line += 1 265 | 266 | if args.check_leaked_google: 267 | ws_stats["B" + str(cur_line)] = 'Leaked HIBP' 268 | ws_stats["C" + str(cur_line)] = leaked_passwords_HIBP 269 | cur_line += 1 270 | 271 | ws_stats["B" + str(cur_line)] = 'Leaked Google' 272 | ws_stats["C" + str(cur_line)] = leaked_passwords_google 273 | cur_line += 1 274 | 275 | chart = BarChart() 276 | chart.add_data( 277 | Reference(ws_stats, range_string=f'Stats!C{start_line+1}:C{cur_line-1}')) 278 | chart.set_categories( 279 | Reference(ws_stats, range_string=f'Stats!B{start_line+1}:B{cur_line-1}')) 280 | chart.title = "Leaked Password" 281 | chart.legend = None 282 | ws_stats.add_chart(chart, f'E{start_line}') 283 | 284 | return max(cur_line, start_line+15) 285 | 286 | def password_topology_stats(ws_stats, start_line, args, list_users, list_user_password, list_regex, dictionary_list): 287 | username_based = 0 288 | username_same = 0 289 | repeated_word = 0 290 | 291 | regex_results = {} 292 | dictionary_results = {} 293 | 294 | for cur_user in list_user_password: 295 | 296 | # don't count on password history 297 | if re.search(r"_history[0-9]*$", cur_user): 298 | continue 299 | 300 | # check if password is the username 301 | if cur_user.upper() == list_user_password[cur_user].upper(): 302 | username_same += 1 303 | else: 304 | # check if password is derived from username 305 | r = convert_into_l33t_regex(cur_user) 306 | r_i = convert_into_l33t_regex(cur_user[::-1]) 307 | 308 | if re.search(r, list_user_password[cur_user]) or re.search(r_i, list_user_password[cur_user]): 309 | username_based += 1 310 | 311 | # check is in the password there is repeated words 312 | for i in range(0, len(list_user_password[cur_user]) - 2): 313 | sub = re.escape(list_user_password[cur_user][i:i+3]) 314 | 315 | if len(re.findall(sub, list_user_password[cur_user])) > 1: 316 | 317 | repeated_word += 1 318 | break 319 | 320 | # check password on regex 321 | try: 322 | for reg in list_regex: 323 | if re.search(reg, list_user_password[cur_user]): 324 | try: 325 | regex_results[reg] += 1 326 | except: 327 | regex_results[reg] = 1 328 | except: 329 | pass 330 | 331 | # check password on dictionary 332 | try: 333 | for w in dictionary_list: 334 | r = convert_into_l33t_regex(w) 335 | if re.search(r, list_user_password[cur_user]): 336 | if w in dictionary_results: 337 | dictionary_results[w] += 1 338 | else: 339 | dictionary_results[w] = 1 340 | except: 341 | pass 342 | # create password type stats 343 | password_type = {} 344 | 345 | password_type["Same as username"] = username_same 346 | password_type["Based on username"] = username_based 347 | password_type["Repeated word"] = repeated_word 348 | password_type["Others"] = len(list_user_password) - username_same - username_based - repeated_word 349 | 350 | 351 | try: 352 | for i in list_regex: 353 | try: 354 | password_type["Based on \"" + str(i) + "\""] = regex_results[i] 355 | except: 356 | pass 357 | except: 358 | pass 359 | 360 | try: 361 | for i in dictionary_results: 362 | try: 363 | password_type["Based on \"" + 364 | str(i) + "\""] = dictionary_results[i] 365 | except: 366 | pass 367 | except: 368 | pass 369 | 370 | ws_stats[f"B{start_line}"] = "Password Topology" 371 | ws_stats[f"B{start_line}"].font = Font(bold=True) 372 | 373 | cur_line = start_line+1 374 | 375 | for i in sorted(password_type.items(), key=operator.itemgetter(1), reverse=True): 376 | if i[1] > 0: 377 | ws_stats["B" + str(cur_line)] = i[0] 378 | ws_stats["C" + str(cur_line)] = i[1] 379 | cur_line += 1 380 | 381 | chart = BarChart() 382 | chart.add_data( 383 | Reference(ws_stats, range_string=f'Stats!C{start_line+1}:C{cur_line-1}')) 384 | chart.set_categories( 385 | Reference(ws_stats, range_string=f'Stats!B{start_line+1}:B{cur_line-1}')) 386 | chart.title = "Password Topology" 387 | chart.legend = None 388 | ws_stats.add_chart(chart, f'E{start_line}') 389 | 390 | return max(cur_line, start_line+15) 391 | 392 | 393 | def levenshtein_distance_stats(ws_stats, start_line, args, list_users, list_user_password, list_user_password_with_history): 394 | 395 | cut_distance = 14 396 | 397 | levenshtein_distance = {} 398 | for i in range(0, cut_distance+2): 399 | levenshtein_distance[i] = 0 400 | 401 | for cur_user in list_user_password: 402 | 403 | # don't count on password history 404 | if re.search(r"_history[0-9]*$", cur_user): 405 | continue 406 | 407 | # check Levenshtein distance if history0 exist 408 | try: 409 | 410 | if list_user_password_with_history[cur_user + "_history0"]: 411 | 412 | if args.ignore_case: 413 | dis = distance(list_user_password[cur_user].upper(), list_user_password_with_history[cur_user + "_history0"].upper()) 414 | else: 415 | dis = distance(list_user_password[cur_user], list_user_password_with_history[cur_user + "_history0"]) 416 | 417 | if dis > cut_distance: 418 | levenshtein_distance[cut_distance+1] += 1 419 | else: 420 | levenshtein_distance[dis] += 1 421 | 422 | except: 423 | pass 424 | 425 | 426 | 427 | 428 | ws_stats[f"B{start_line}"] = "Levenshtein distance" 429 | ws_stats[f"B{start_line}"].font = Font(bold=True) 430 | 431 | cur_line = start_line + 1 432 | 433 | if args.show_not_cracked and list_users: 434 | ws_stats["B" + str(cur_line)] = "NC" 435 | ws_stats["C" + str(cur_line)] = len(list_users) - \ 436 | len(list_user_password) 437 | cur_line += 1 438 | 439 | ws_stats["B" + str(cur_line)] = "Unk" 440 | ws_stats["C" + str(cur_line)] = len(list_user_password) - \ 441 | sum(levenshtein_distance.values()) 442 | cur_line += 1 443 | 444 | max_dist = max([i for i in range(cut_distance+1) if levenshtein_distance[i] != 0]+[0]) 445 | 446 | if levenshtein_distance[cut_distance+1] != 0: 447 | max_dist = cut_distance 448 | 449 | for i in range(max_dist+1): 450 | ws_stats["B" + str(cur_line)] = i 451 | ws_stats["C" + str(cur_line)] = levenshtein_distance[i] 452 | cur_line += 1 453 | 454 | if levenshtein_distance[cut_distance+1] != 0: 455 | ws_stats["B" + str(cur_line)] = f">{cut_distance}" 456 | ws_stats["C" + str(cur_line)] = levenshtein_distance[cut_distance+1] 457 | cur_line += 1 458 | 459 | chart = BarChart() 460 | chart.add_data( 461 | Reference(ws_stats, range_string=f'Stats!C{start_line+1}:C{cur_line-1}')) 462 | chart.set_categories( 463 | Reference(ws_stats, range_string=f'Stats!B{start_line+1}:B{cur_line-1}')) 464 | chart.title = "Levenshtein distance" 465 | chart.legend = None 466 | ws_stats.add_chart(chart, f'E{start_line}') 467 | 468 | return max(cur_line, start_line+15) 469 | -------------------------------------------------------------------------------- /test_files/dictionary.txt: -------------------------------------------------------------------------------- 1 | password 2 | test -------------------------------------------------------------------------------- /test_files/password.txt: -------------------------------------------------------------------------------- 1 | user:password 2 | user2:password 3 | user3:password 4 | user3_history0:123488726 5 | user4:abababab 6 | user5:qwerty 7 | user6:qwerty 8 | user7:test 9 | user7_history0:asdasd 10 | user8:password 11 | user8_history0:PaSSworD 12 | user8_history1:PASSWORD 13 | user8_history2:12345567 14 | user9:$HEX[706124243a776f726421] 15 | testuser:t3s7us3r 16 | qwerty:qwerty 17 | aaaa:abcabcabc 18 | bbbb:p4ssword 19 | cccc:testing123 20 | dddd:t3stttt 21 | eeee:zxzxzxzxz 22 | ffff:passzssword12 23 | gggg:hhhhhhh 24 | hhhh:qwqwqwqw 25 | iiii:IiIihhhh -------------------------------------------------------------------------------- /test_files/regexs.txt: -------------------------------------------------------------------------------- 1 | pa.+wor.+ 2 | te.+ing.+ -------------------------------------------------------------------------------- /test_files/users.txt: -------------------------------------------------------------------------------- 1 | aaaa 2 | bbbb 3 | cccc 4 | dddd 5 | eeee 6 | ffff 7 | gggg 8 | hhhh 9 | iiii 10 | qwerty 11 | testuser 12 | user 13 | user2 14 | user3 15 | user4 16 | user5 17 | user6 18 | user7 19 | user8 20 | user9 21 | notcrackeduser 22 | otheruser 23 | notcrackeduser2 24 | notcrackeduser23 --------------------------------------------------------------------------------