├── pwd_leet.conf ├── pwd_common.conf ├── setup.sh ├── add-password-delimit ├── .gitignore ├── LICENCE ├── README.md └── pwdlyser.py /pwd_leet.conf: -------------------------------------------------------------------------------- 1 | 4,a 2 | 1,i 3 | 3,e 4 | !,i 5 | 5,s 6 | 6,g 7 | 0,o 8 | $,s 9 | 7,t 10 | @,a 11 | 9,g 12 | -------------------------------------------------------------------------------- /pwd_common.conf: -------------------------------------------------------------------------------- 1 | 123456 2 | password 3 | qwerty 4 | abc123 5 | iloveyou 6 | letmein 7 | system 8 | admin 9 | starwars 10 | dragon 11 | 111111 12 | football 13 | welcome 14 | england 15 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "[!] Sudo needed to copy to /etc/ and /usr/ directories:" 4 | echo "- Pwdlyser.py to /usr/local/bin/" 5 | echo "- Config files to /etc/pwdlyser/" 6 | sudo -H cp pwdlyser.py /usr/local/bin/pwdlyser 7 | sudo -H mkdir /etc/pwdlyser/ 8 | sudo -H cp *.conf /etc/pwdlyser/ 9 | -------------------------------------------------------------------------------- /add-password-delimit: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | # Script can be used to add a prefix to a password list for the input format of username:password 4 | 5 | import fileinput 6 | import sys 7 | import argparse 8 | 9 | parser = argparse.ArgumentParser('Password Analyser Delimeter') 10 | parser.add_argument('--path',dest='file_path',required=True,help='Enter the file path of the password list to delimit via ":"') 11 | args = parser.parse_args() 12 | 13 | 14 | 15 | print "[+] Opening file " + args.file_path + " for amendment..." 16 | 17 | with open(args.file_path, 'r') as f: 18 | lines = f.readlines() 19 | 20 | lines = ['NONE:'+line for line in lines] 21 | 22 | with open(args.file_path + "_delimited", 'w') as x: 23 | x.writelines(lines) 24 | 25 | print "[+] File " + args.file_path + "_delimited has been saved." 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | This software is provided freely, but any use of the software must be attributed to the author as per this 2 | licence. Commercial licences are not currently available, and the software is not provided for the use of 3 | integration in to automated systems or for redistribution or re-branding. 4 | 5 | Neither the names of the author, nor the names of any contributors to ‘the software’, nor any of their 6 | trademarks or service marks, may be used to endorse or promote products derived from this original work 7 | without express prior permission of the author. 8 | 9 | Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or 10 | otherwise, shall the Licensor be liable to anyone for any direct, indirect, special, incidental, or 11 | consequential damages of any character arising as a result of this License or the use of the Original 12 | Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or 13 | malfunction, or any and all other commercial damages or losses. This limitation of liability shall not 14 | apply to the extent applicable law prohibits such limitation. 15 | 16 | You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by 17 | law, and Licensor promises not to interfere with or be responsible for such uses by You. 18 | 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | The 'pwdlyser' tool is a Python-based CLI script that automates the arduous process of manually reviewing cracked passwords during password audits following security assessments or penetration tests. There are likely some false positives/negatives, so please use at your own discretion. 3 | 4 | ## Installation 5 | The installation of this tool is fairly straight forward. Clone the repo and use the following steps: 6 | ``` 7 | chmod +x setup.sh 8 | ./setup.sh 9 | ``` 10 | 11 | ## Input: Passwords 12 | Lists can be specified using the ```-p [path/to/file]``` argument, and should be colon delimited as ```username:password```, or just password (however, this will just assess passwords and use a generic username for each). No headers are necessary. 13 | 14 | Should you only want to analyse passwords, just enter a colon (":") before each password in the list, which will just output blank usernames. To automate this I've added a script 'add-delimit.py' that will input a list of passwords (only) and append the colon to the start. 15 | 16 | ## Summary Output 17 | 18 | One of the newest features of pwdlyser is the ability to quickly generate a management-level summary of the password health within the organisation. This output is provided in a paragraph format and dynamically details each of the respective checks (i.e. keyboard patterns, common passwords, etc). I would suggest using this for management summaries, whilst the ```-oR``` option should be used for a more technical reporting output. 19 | 20 | ## Reporting Output 21 | 22 | The ```-oR``` argument can be used to generate a list of usernames and passwords that have been analysed within each of the respective checks (shared password reuse, variation of usernames as passwords, etc) in a more technical level. The passwords are masked, except the start and a certain amount of end characters (e.g. ```P*****rd1```). This output is more suitable for a technical commentary within a penetration testing or security assessment report. 23 | 24 | ## General Usage 25 | There are a range of input arguments that can be used, but for a simple 'common password' search through a list use the ```-c``` argument to initiate the check. This will import the default ```pwd_common.conf``` file and use it as a basis to compare against the password list. Passwords and the common passwords are both converted to lower-case, with the inputted passwords also being 'de-leeted' and converted back to alpha characters (i.e. 3 to e). The reason for this, even though some passwords may end up reading 'iadmin' instead of '!admin' is that this is only a basic comparison, but it seems to work well. 26 | 27 | Other arguments also include the check for any users that have their username as part of their password. This can be run using the ```-up``` or ```--user-as-pass``` arugments. 28 | 29 | To display any passwords that have a minimum length less than 9 characters use ```-l 9```. The int can be changed to whatever the password policy is, although you should also really ensure that you verify against best-practice too. 30 | 31 | Basic ('de-leeted') searches can be run using ```-S [word]```, with an exact search can be run using ```--exact [word]```. The exact search does not modify any characters for comparison and thus allows you to check for any passwords containing '123' or 'P4$$', for example. 32 | 33 | Organisation names often appear within passwords, at least from my experience during internal penetration tests. To check for this, a similar search to the 'basic' search is run, although the only difference is that the 'Description' will state 'Organisation name: [name]' on screen instead. Run this using ```-o [orgname or acronym]```. 34 | 35 | If you want to verify whether you were able to crack the passwords for any admin accounts then you can put the usernames (only) in a file and use ```--admin [path/to/file]``` to display any of the admin passwords that could be cracked. This is of course useful for any escalation or pivoting that you may need to do, or to ensure that administrators are not using weak or reusing passwords. 36 | 37 | For simple searches for usernames that may be in the password list use ```-u [username/part of username]```. This list also works with email:passwords, it doesn't discriminate. Part, or excerpts of usernames can also be used. 38 | 39 | To just identify the top N of passwords, i.e. frequency analysis, use the ```-f [int]``` argument and specify the number of passwords you want to return. This will need to be an integer. 40 | 41 | Other options can be seen within the ```-h``` menu or below: 42 | 43 | ``` 44 | usage: pwdlyser [-h] [--all] [--admin ADMIN_PATH] [-c] [--char-analysis] 45 | [--date] [-e] [--exact EXACT_SEARCH] [-f FREQ_ANAL] 46 | [-fl FREQ_LEN] [-k] [-l MIN_LENGTH] [-m] 47 | [-mc MASKS_RESULTS_COUNT] [-o ORG_NAME] [-oR] -p PASS_LIST 48 | [-S BASIC_SEARCH] [-s] [-u USER_SEARCH] [-up] [-w] [--summary] 49 | 50 | Password Analyser 51 | 52 | optional arguments: 53 | -h, --help show this help message and exit 54 | --all, -A Run all standard tests. Can be combined with -o [org- 55 | name], --summary, --admin [path] 56 | --admin ADMIN_PATH Import line separated list of Admin usernames to check 57 | password list 58 | -c, --common Check against list of common passwords 59 | --char-analysis Perform character-level analysis 60 | --date Check for common date/day passwords 61 | -e, --entropy Output estimated entropy for the top 10 passwords (by 62 | frequency used) 63 | --exact EXACT_SEARCH Perform a search using the exact string. 64 | -f FREQ_ANAL, --frequency FREQ_ANAL 65 | Perform frequency analysis 66 | -fl FREQ_LEN, --length-frequency FREQ_LEN 67 | Perform frequency analysis on password length 68 | -k, --keyboard-pattern 69 | Identify common keyboard pattern usage within password 70 | lists 71 | -l MIN_LENGTH, --length MIN_LENGTH 72 | Display passwords that do not meet the minimum length 73 | -m, --mask Perform common Hashcat mask analysis 74 | -mc MASKS_RESULTS_COUNT, --mask-count MASKS_RESULTS_COUNT 75 | (Optional) Specify the number of mask to output for 76 | the -m / --masks option 77 | -o ORG_NAME, --org-name ORG_NAME 78 | Enter the organisation name to identify any users that 79 | will be using a variation of the word for their 80 | password. Note: False Positives are possible 81 | -oR Output format set for reporting with "- " prefix 82 | -p PASS_LIST, --pass-list PASS_LIST 83 | Enter the path to the list of passwords, either in the 84 | format of passwords, or username:password. 85 | -S BASIC_SEARCH, --search BASIC_SEARCH 86 | Run a basic search using a keyword. Non-alpha 87 | characters will be stripped, i.e. syst3m will become 88 | systm (although this will be compared against the same 89 | stripped passwords 90 | -s, --shared Display any reused/shared passwords. 91 | -u USER_SEARCH, --user USER_SEARCH 92 | Return usernames that match string (case insensitive) 93 | -up, --user-as-pass Check for passwords that use part of the username 94 | -w, --clean-wordlist Enable this flag to append cleaned (no trailing 95 | numerics) to a wordlist at wordlist-cleaned.txt 96 | --summary Use --summary to provide a concise report-friendly 97 | output. 98 | ``` 99 | 100 | ## Example Outputs 101 | 102 | ### Basic Search 103 | ``` 104 | > pwdlyser -p sample-file -S pass 105 | 106 | ##### # # ##### # # # #### ###### ##### 107 | # # # # # # # # # # # # # 108 | # # # # # # # # #### ##### # # 109 | ##### # ## # # # # # # # ##### 110 | # ## ## # # # # # # # # # 111 | # # # ##### ###### # #### ###### # # 112 | 113 | ---- Password analysis & reporting tool -- v1.0.0 ---- 114 | 115 | ------------------------------: ------------------------------ : ------------------------------ 116 | Username : Password : Description 117 | ------------------------------: ------------------------------ : ------------------------------ 118 | user1 : password1 : Variation of pass 119 | : testpass : Variation of pass 120 | ``` 121 | 122 | ### User As Pass 123 | 124 | ``` 125 | > pwdlyser -p sample-file -up 126 | 127 | ##### # # ##### # # # #### ###### ##### 128 | # # # # # # # # # # # # # 129 | # # # # # # # # #### ##### # # 130 | ##### # ## # # # # # # # ##### 131 | # ## ## # # # # # # # # # 132 | # # # ##### ###### # #### ###### # # 133 | 134 | ---- Password analysis & reporting tool -- v1.0.0 ---- 135 | 136 | ------------------------------: ------------------------------ : ------------------------------ 137 | Username : Password : Description 138 | ------------------------------: ------------------------------ : ------------------------------ 139 | lenovo : L3n0vo! : Variation of lenovo 140 | Bluecoat : *blu3c0at$ : Variation of Bluecoat 141 | system : sy$t3m! : Variation of system 142 | ``` 143 | 144 | ### Common Passwords 145 | 146 | ``` 147 | > pwdlyser -p sample-file -c 148 | 149 | ##### # # ##### # # # #### ###### ##### 150 | # # # # # # # # # # # # # 151 | # # # # # # # # #### ##### # # 152 | ##### # ## # # # # # # # ##### 153 | # ## ## # # # # # # # # # 154 | # # # ##### ###### # #### ###### # # 155 | 156 | ---- Password analysis & reporting tool -- v1.0.0 ---- 157 | 158 | ------------------------------: ------------------------------ : ------------------------------ 159 | Username : Password : Description 160 | ------------------------------: ------------------------------ : ------------------------------ 161 | user1 : password1 : Variation of password 162 | user4 : l3tme1n_* : Variation of letmein 163 | ``` 164 | 165 | ### Frequency 166 | ``` 167 | > pwdlyser -p sample-file -f 3 168 | 169 | ##### # # ##### # # # #### ###### ##### 170 | # # # # # # # # # # # # # 171 | # # # # # # # # #### ##### # # 172 | ##### # ## # # # # # # # ##### 173 | # ## ## # # # # # # # # # 174 | # # # ##### ###### # #### ###### # # 175 | 176 | ---- Password analysis & reporting tool -- v1.0.0 ---- 177 | 178 | ------------------------------: ------------------------------ 179 | Password : Frequency 180 | ------------------------------: ------------------------------ 181 | password1 : 3 182 | blu3c0at! : 1 183 | Friday924 : 1 184 | ``` 185 | 186 | ### Report Format (-oR) 187 | ``` 188 | > pwdlyser -p sample-file -c -oR 189 | 190 | ##### # # ##### # # # #### ###### ##### 191 | # # # # # # # # # # # # # 192 | # # # # # # # # #### ##### # # 193 | ##### # ## # # # # # # # ##### 194 | # ## ## # # # # # # # # # 195 | # # # ##### ###### # #### ###### # # 196 | 197 | ---- Password analysis & reporting tool -- v1.0.0 ---- 198 | 199 | 200 | The following user accounts were found to have a password that was a variation 201 | of the most common user passwords, which can include 'password', 'letmein', 202 | '123456', 'admin', 'iloveyou', 'friday', or 'qwerty': 203 | - user2 : P4****rd1 204 | - user5 : Pa***ord 205 | - user1 : Dec****r16 206 | - user9 : zaq****23 207 | 208 | . 209 | . 210 | . 211 | ``` 212 | 213 | ### Mask Analysis 214 | 215 | One of the more useful features for active penetration testing is the ability to analyse the more common password masks for the cracked passwords, and to then reuse them within further Hashcat attacks. 216 | 217 | ``` 218 | pwdlyser -p sample-file.txt -m 219 | 220 | ##### # # ##### # # # #### ###### ##### 221 | # # # # # # # # # # # # # 222 | # # # # # # # # # #### ##### # # 223 | ##### # # # # # # # # # # ##### 224 | # ## ## # # # # # # # # # 225 | # # # ##### ###### # #### ###### # # 226 | 227 | ---- Password analysis & reporting tool --- v2.4.2 ---- 228 | 229 | [!] Running analysis with 'user:password' delimitation 230 | 231 | ------------------------------: ------------------------------ : ------------------------------ 232 | Hashcat Mask : Mask Length : Occurrences 233 | ------------------------------: ------------------------------ : ------------------------------ 234 | ?u?l?l?l?l?l?l?u?d : 9 : 601 235 | ?u?l?l?l?l?d?d?d : 8 : 266 236 | ?u?l?l?l?l?l?l?d?d : 9 : 152 237 | ?u?l?l?l?l?l?l?l?l?d : 10 : 132 238 | ?u?l?l?l?l?l?l?l?l?l?d?d : 13 : 72 239 | ?u?l?l?l?l?l?l?l?l?d?d : 11 : 62 240 | ?u?l?l?l?l?d?d?d?d : 9 : 55 241 | ?l?l?l?l?l?u?u?u?u?u?d?d?d : 13 : 49 242 | ?u?l?l?l?l?l?l?l?d?s : 10 : 48 243 | ?u?l?l?l?l?l?l?d : 8 : 46 244 | ?u?l?l?l?l?l?d?d?d : 9 : 42 245 | ?u?l?l?l?l?l?d?d?d?d : 10 : 38 246 | ?u?l?l?l?l?d?d?s : 8 : 30 247 | ?u?l?l?l?l?l?l?l?l?d : 10 : 29 248 | ?u?l?l?l?l?l?l?d?d?d?d : 11 : 28 249 | ?u?l?l?l?l?l?l?l?l?l?d?d : 12 : 21 250 | ?u?l?l?l?l?l?l?d?d?d?d : 11 : 20 251 | ?u?l?l?l?d?d?d?d : 8 : 19 252 | ?u?l?l?l?l?l?l?d?d?s : 10 : 19 253 | ?u?l?l?l?l?l?l?d?d?d : 10 : 19 254 | ?u?l?l?l?l?l?l?l?d?d?d?d : 12 : 17 255 | ?u?l?l?l?l?l?d?d?s : 9 : 15 256 | ?u?l?l?l?l?l?l?l?l?d?d?d?d : 13 : 13 257 | ?u?l?l?l?l?l?l?l?d?d?s : 11 : 12 258 | ?s?l?l?l?l?l?l?l?l?d : 10 : 11 259 | ``` 260 | 261 | ### Summary Output (--summary) 262 | 263 | 264 | > pwdlyser -p sample-file.txt --summary -o SAMPLE-ORG --admin admin-user-list.txt 265 | 266 | A password audit was performed against the extracted password hashes from the specified system. Password cracking tools and methods were used to enumerate the plaintext password counterparts, and as such not all of the passwords were able to be identified. In total, there were 2448 username and password combinations that were obtained. 267 | 268 | As part of the password audit, the top 10 most commonly used passwords within the organisation have been compiled. This list has been broken up with the password, the percentage of the total passwords, and the numeric value of the total passwords: 269 | - Password01 : 31% | 481/2448 270 | - Germany01 : 4% | 66/2448 271 | - 123qwert!ZXC : 3% | 49/2448 272 | - letm31n! : 2% | 38/2448 273 | - Password2! : 2% | 35/2448 274 | - starw4r$ : 0% | 22/2448 275 | - Password2 : 1% | 14/2448 276 | - W3lc0ome01 : 0% | 13/2448 277 | - Bu773rfl1es : 0% | 12/2448 278 | - letm31n234 : 0% | 10/2448 279 | 280 | Alongside the list of the most common passwords used within the organisation, the top 10 most common password lengths were analysed and the results can be seen below in the format of the character length, along with the percentage of the total passwords for each password length: 281 | - Length : 10 : 41% 282 | - Length : 8 : 20% 283 | - Length : 7 : 16% 284 | - Length : 11 : 7% 285 | - Length : 13 : 6% 286 | - Length : 16 : 3% 287 | - Length : 9 : 1% 288 | - Length : 15 : 1% 289 | - Length : 14 : 0% 290 | - Length : 12 : 0% 291 | 292 | One of the biggest threats to organisations in relation to the passwords used by users and administrators is the use of passwords that are exactly the same, or a variation of the more commonly used passwords. Overall, there were 603 passwords that were found to have a variation of one of these common words or phrases. Some of these passwords include 'password', 'qwerty', 'starwars', 'system', 'admin', 'letmein', and 'iloveyou'. Further details can be seen within the 'pwd_common.conf' file at https://www.github.com/ins1gn1a/pwdlyser. 293 | 294 | As part of the wider password analysis, each password was assessed and compared to the commonly used keyboard patterns. These keyboard patterns are defined by the QWERTY layout, where a password is made up of characters in close proximity, such as qwer, zxcvbn, qazwsx, and so on. In total, there were 59 passwords in use that had at least one of these variations. 295 | 296 | There were 10 passwords that were identified as having a password set that was a variation of the username; this includes additional prefixed or suffixed characters, substitutions within the word (i.e. 3 instead of e), or the username as it appears. Penetration testers, and more importantly attackers, will often check system or administrative accounts that have a variation of the username set as the password, and as such it is critical that organisations do not use this convention for password security. 297 | 298 | The organisation name, or a variation of the name (such as an abbreviation) 'SAMPLE-ORG' was found to appear within 14 of the passwords that were able to be obtained during the password audit. For any system or administrative user accounts that have a variation of the company name as their password, it is highly recommended that the passwords are changed to prevent targeted guessing attacks. 299 | 300 | Finally, there were 5 Domain administrative accounts (Domain Admins, Enterprise Admins, etc.) that were able to be compromised through password analysis. The account names and their respective passwords (masked) can be seen below: 301 | - user.admin1 : Run****3! 302 | - sys.admin : $!x***az1 303 | - svc-wsus : a4a*****tYc 304 | - user.admin2 : P4****rd2 305 | - user2 : P4****rd!2 306 | 307 | -------------------------------------------------------------------------------- /pwdlyser.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | __author__ = "Adam Govier" 4 | __license__ = "MIT" 5 | __version__ = "2.6.0" 6 | __maintainer__ = "ins1gn1a" 7 | __status__ = "Production" 8 | 9 | import sys, os 10 | import argparse 11 | from string import digits 12 | import re 13 | from collections import Counter 14 | from collections import defaultdict 15 | import collections 16 | import math 17 | 18 | parser = argparse.ArgumentParser(description='Password Analyser and Reporting Tool') 19 | 20 | group = parser.add_mutually_exclusive_group(required=True) 21 | group.add_argument('-p','--pass-list',dest='pass_list',help='Enter the path to the list of passwords in the format of username:password (see README usage for additional information).') 22 | 23 | summary_group = parser.add_mutually_exclusive_group(required=False) 24 | summary_group.add_argument('--all','-A',dest='print_all',help='Run all standard tests and display the output in format aimed at a more technical audience. Can be combined with -o [org-name], --summary, --admin [path]',action='store_true',required=False) 25 | summary_group.add_argument('--report','-oR',dest='output_report',help='Display a descriptive output that is suitable for a more technical audience. This output provides usernames and partially-masked passwords, and can be used with any of the individual tests (such as -e, -m, -c, etc.)',action='store_true',default=False,required=False) 26 | summary_group.add_argument('--summary',dest='summary',help='Use --summary to provide a descriptive management-summary output.',required=False,action='store_true',default=False) 27 | 28 | parser.add_argument('--admin',dest='admin_path',help='Import a line separated list of administrator usernames (Domain Admins, Enterprise Admins, etc.) to check against the cracked password list',required=False) 29 | parser.add_argument('-c','--common',dest='common_pass',help='Analyse passwords against a list of common passwords and their variations',action='store_true',default=False,required=False) 30 | parser.add_argument('--char-analysis',dest='char_anal',help='Perform character-level analysis, useful for penetration and security testers',required=False,action='store_true',default=False) 31 | parser.add_argument('--date',dest='date_day',help='Review passwords that use a variation of dates, days, months, or years',required=False,action='store_true',default=False) 32 | parser.add_argument('-e','--entropy',dest='entropy',help='Output the estimated entropy for the top 10 passwords (by frequency of bits)',action='store_true',default=False) 33 | parser.add_argument('--exact',dest='exact_search',help='Perform a search using an exact input string',required=False) 34 | parser.add_argument('-f','--frequency',dest='freq_anal',help='Perform analysis of the frequency of the top N passwords. Usage example: "-f 10"',required=False,type=int) 35 | parser.add_argument('-fl','--length-frequency',dest='freq_len',help='Perform analysis on the most frequently used password lengths. Usage example: "-fl 15"',required=False,type=int) 36 | parser.add_argument('--hashes',dest='hashes',help='Allows for the password reuse argument (-r / --reuse) to utilise hashes in the form of USER:HASH:PASSWORD',action='store_true',default=False) 37 | parser.add_argument('-k','--keyboard-pattern',dest='keyboard_pattern',help='Identify common keyboard pattern usage within password lists, such as passwords using "zxc123"',required=False,action='store_true',default=False) 38 | parser.add_argument('-l','--length',dest='min_length',help='Display passwords that do not meet the minimum length specified. Usage example: "-l 8"',type=int,required=False) 39 | parser.add_argument('-m','--mask',dest='masks',help='Display the most commonly used Hashcat masks. This is extremely useful for further cracking attacks',action='store_true',required=False,default=False) 40 | parser.add_argument('-mc','--mask-count',dest='masks_results_count',help='(Optional) Specify the number of mask to output for the -m / --masks option',default=25,required=False,type=int) 41 | parser.add_argument('-o','--org-name',dest='org_name',help='Enter the organisation name or abbreviation to identify any users that have a variation in their password. Usage exmaple: "-o google"',required=False) 42 | parser.add_argument('-r','--reuse',dest='reuse_pass',help='List user accounts and masked passwords that re-use passwords between low-privileged and high-privileged accounts.',required=False,action='store_true',default=False) 43 | parser.add_argument('-S','--search',dest='basic_search',help='Run a basic search using a keyword. Non-alpha characters will be stripped, i.e. syst3m will become systm (although this will be compared against the same stripped passwords',required=False) 44 | parser.add_argument('-s','--shared',dest='shared_pass',help='Display any passwords that appear more than once in the list. This is useful for identifying accounts that reuse passwords, such as service accounts or administrators',required=False,action='store_true',default=False) 45 | parser.add_argument('-u','--user',dest='user_search',help='Search for usernames that contain all or part of the input string (case insensitive). Usage example: "-u ins1g"',required=False) 46 | parser.add_argument('-up','--user-as-pass',dest='user_as_pass',help='Check for passwords that use part or all of the username',required=False,action='store_true',default=False) 47 | parser.add_argument('-w','--clean-wordlist',dest='clean_pass_wordlists',help='Enable this flag to append cleaned (no trailing numerics) to a wordlist at wordlist-cleaned.txt. Re-using this wordlist in Hashcat or John can be useful when paired with a strong rule-list',required=False,action='store_true',default=False) 48 | parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) 49 | args = parser.parse_args() 50 | 51 | pass_list = args.pass_list 52 | admin_list = args.admin_path 53 | organisation = args.org_name 54 | issue_old = None 55 | 56 | rows, columns = os.popen('stty size', 'r').read().split() 57 | 58 | banner = "\n ##### # # ##### # # # #### ###### ##### \n" 59 | banner = banner + " # # # # # # # # # # # # # \n" 60 | banner = banner + " # # # # # # # # # #### ##### # # \n" 61 | banner = banner + " ##### # # # # # # # # # # ##### \n" 62 | banner = banner + " # ## ## # # # # # # # # # \n" 63 | banner = banner + " # # # ##### ###### # #### ###### # # \n\n" 64 | banner = banner + " ---- Password analysis & reporting tool --- v" + __version__ + " ----\n" 65 | 66 | def update_check(): 67 | 68 | try: 69 | import urllib 70 | from urllib import request 71 | raw_update_data = urllib.request.urlopen("https://raw.githubusercontent.com/ins1gn1a/pwdlyser/master/pwdlyser.py").read(200) 72 | update_data = (raw_update_data.decode()).split("\n") 73 | for line in update_data: 74 | if "__version__" in line: 75 | v_check = str(line.split('"')[1]) 76 | if str(__version__) != v_check: 77 | print ("[!] New version of pwdlyser (v " + v_check + ") is available on GitHub: https://wwww.github.com/ins1gn1a/pwdlyser \n") 78 | except: 79 | print ("[!] Auto-update check not performed. Please check for a new version manually at https://www.github.com/ins1gn1a/pwdlyser \n") 80 | 81 | # Input function 82 | def import_file_to_list(path): 83 | with open(path) as file: 84 | out_var = file.read().splitlines() 85 | return out_var 86 | 87 | # Check for admin accounts (from list) that were compromised 88 | def check_admin(user,pwd): 89 | admin_list = import_file_to_list(args.admin_path) 90 | for admin in admin_list: 91 | if admin.lower().rstrip() == user.lower().rstrip(): 92 | if args.summary: 93 | return (("- " + user + " : " + password_masking(pwd))) 94 | elif args.output_report: 95 | print_report(user + " : " + password_masking(pwd)) # + " [Variation of '" + + "']") 96 | else: 97 | output_pass(user,pwd,"Admin: " + admin) 98 | 99 | # Output to STDOUT 100 | def output_pass(username,password,issue): 101 | if password.rstrip() == "": 102 | end = "" 103 | else: 104 | end = ":" 105 | 106 | if issue.rstrip() == "": 107 | end_delim = "" 108 | else: 109 | end_delim = ":" 110 | 111 | print (str(username.ljust(30)),end=end.ljust(5),flush=True) 112 | print (str(password.ljust(35)),end=end_delim.ljust(5),flush=True) 113 | print (issue) 114 | 115 | def print_report(u): 116 | print ("- " + u) 117 | 118 | # Check for inputted min length 119 | def check_min_length(password,min): 120 | if (len(password) < min) or (password.rstrip() == "*******BLANK-PASS*******"): 121 | if args.output_report: 122 | print_report(user + " : " + password_masking(pwd)) 123 | else: 124 | output_pass(user,pwd,"Length < " + str(args.min_length)) 125 | 126 | def check_user_search(user,password,term): 127 | if term.lower() in user.lower(): 128 | if args.output_report: 129 | print_report(user) 130 | else: 131 | output_pass(user,password,"Username Search: " + term) 132 | 133 | def check_exact_search(user,password,term): 134 | if term in password: 135 | if args.output_report: 136 | print_report(user + " : " + password_masking(password)) 137 | else: 138 | output_pass(user,password,"Term " + term + " in password") 139 | 140 | # Check for org name (reused code from below, laziness) 141 | def check_org_name(user,password,org): 142 | x = 0 143 | pwd_unleet = password 144 | leet_list = reverse_leet_speak() 145 | for line in leet_list: 146 | if "," in line: 147 | char_change = line.split(",") 148 | else: 149 | continue 150 | try: 151 | pwd_unleet = (pwd_unleet.replace(char_change[0],char_change[1])).lower() 152 | search = org.lower() 153 | except: 154 | continue 155 | if (search in pwd_unleet): 156 | if args.output_report: 157 | print_report(user + " : " + password_masking(password)) 158 | elif args.summary: 159 | return (password) 160 | else: 161 | output_pass(user,password,"Variation of org name " + org) 162 | 163 | # Imports leet config file and processes each mutation 164 | def reverse_leet_speak(): 165 | if (os.path.exists("/etc/pwdlyser/pwd_leet.conf")): 166 | conf_pwdleet = "/etc/pwdlyser/pwd_leet.conf" 167 | else: 168 | try: 169 | conf_pwdleet = "pwd_leet.conf" 170 | except: 171 | sys.exit("[!] Cannot locate pwd_leet.conf. Try running 'setup.sh' again.") 172 | with open(conf_pwdleet) as leetconf: 173 | leet_list = leetconf.read().splitlines() 174 | return leet_list 175 | 176 | # Check for user stating as password 177 | def check_user_as_pass(user,pwd): 178 | if user.rstrip() == "NONE" or user.rstrip() == "": 179 | return 180 | 181 | if args.summary: 182 | tmp = check_basic_search(user,pwd,user) 183 | 184 | if tmp: 185 | return (1) 186 | else: 187 | check_basic_search(user,pwd,user) 188 | 189 | # Checks for variation of input based upon removal of leetspeak 190 | def check_basic_search(user,password,search): 191 | x = 0 192 | pwd_unleet = password 193 | leet_list = reverse_leet_speak() 194 | 195 | for line in leet_list: 196 | if "," in line: 197 | char_change = line.split(",") 198 | else: 199 | continue 200 | try: 201 | pwd_unleet = (pwd_unleet.replace(char_change[0],char_change[1])).lower() 202 | search = search.lower() 203 | except: 204 | continue 205 | if (search in pwd_unleet): 206 | if args.output_report: 207 | print_report(user + " : " + password_masking(password)) 208 | elif args.summary: 209 | return True 210 | else: 211 | output_pass(user,password,"Variation of " + search) 212 | 213 | # Common password check from import list - List can be appended to 214 | def check_common_pass(user,password): 215 | x = 0 216 | out_issue = "" 217 | leet_list = reverse_leet_speak() 218 | pwd_unleet = password 219 | tmp_summary_count = 0 220 | 221 | # Import common passwords 222 | if (os.path.exists("/etc/pwdlyser/pwd_common.conf")): 223 | conf_pwdcommon = "/etc/pwdlyser/pwd_common.conf" 224 | else: 225 | try: 226 | conf_pwdcommon = "pwd_common.conf" 227 | except: 228 | sys.exit("[!] Cannot locate pwd_common.conf. Try running 'setup.sh' again.") 229 | with open (conf_pwdcommon) as passcommon: 230 | pass_list = passcommon.read().splitlines() 231 | 232 | # Loop through common passwords list 233 | for common in pass_list: 234 | common = common.lower() 235 | 236 | # Loop through each leet_speak change in imported list 237 | for line in leet_list: 238 | char_change = line.split(",") 239 | 240 | # Amend each 241 | try: 242 | if char_change[0] in password: 243 | pwd_unleet = (pwd_unleet.replace(char_change[0],char_change[1])).lower() 244 | except: 245 | continue 246 | 247 | if (common in pwd_unleet) and (x == 0): 248 | if args.output_report: 249 | print_report(user + " : " + password_masking(password)) 250 | elif args.summary: 251 | tmp_summary_count += 1 252 | else: 253 | out_issue = "Variation of " + common 254 | output_pass(user,password,out_issue) 255 | x += 1 256 | 257 | if args.summary: 258 | return tmp_summary_count 259 | 260 | def check_date_day(user,password): 261 | x = 0 262 | out_issue = "" 263 | 264 | date_day_list = ['january','february','march','april','may','june','july','august','september','october','november','december','monday','tuesday','wednesday','thursday','friday','saturday','sunday'] 265 | 266 | # Loop through each leet_speak change in imported list 267 | for line in date_day_list: 268 | if (line in password.lower()) and (x == 0): 269 | if args.output_report: 270 | print_report(user + " : " + password_masking(password)) 271 | else: 272 | out_issue = "Variation of '" + line.rstrip() + "'" 273 | output_pass(user,password,out_issue) 274 | x += 1 275 | 276 | def hex_decode_pwd(pwd): 277 | if "$HEX[" in pwd: 278 | return (bytes.fromhex(pwd[5:-1]).decode("utf-8")) 279 | else: 280 | return (pwd) 281 | 282 | # output and delimit input list 283 | def delimit_list(list): 284 | list = import_file_to_list(list) 285 | out_list = [] 286 | n = 0 287 | file_line_count = 0 288 | check_hash_delimit = True 289 | try: 290 | for list_entry in list: 291 | file_line_count += 1 292 | if check_hash_delimit: 293 | try: 294 | # Check if user:hash:pass - return n += 1 if True 295 | if (len(list_entry.split(":",2)[1]) >= 24) and (len(list_entry.split(":",2)[2]) > 0): 296 | n += 1 297 | print ("[!] Running analysis with 'user:hash:password' delimitation\n") 298 | check_hash_delimit = False 299 | else: 300 | print ("[!] Running analysis with 'user:password' delimitation\n") 301 | check_hash_delimit = False 302 | except: 303 | n = 0 304 | check_hash_delimit = False 305 | 306 | # Delimits with hash username:hash:password or username:password 307 | if n != 0: 308 | try: # Try to delimit user:hash:password 309 | list_pwd = hex_decode_pwd(list_entry.split(":",2)[2]) 310 | list_stuff = [list_entry.split(":",2)[0],list_pwd] 311 | except: # Except try user:password 312 | try: 313 | list_pwd = list_entry.split(":",2)[1] 314 | list_stuff = [list_entry.split(":",2)[0],list_pwd] 315 | except: # Everything has gone wrong 316 | print ("[!] Can't split input line: " + str(file_line_count)) 317 | 318 | else: 319 | try: 320 | list_both = list_entry.split(":",1) 321 | list_pwd = (hex_decode_pwd(list_both[1])) 322 | list_stuff = (list_both[0],list_pwd) 323 | except: 324 | continue 325 | if (len(list_stuff)) == 1: # Can't remember what this does 326 | list_stuff.append("") 327 | out_list.append(list_stuff) 328 | except: 329 | sys.exit("[!] Cannot delimit the input list. Check that input is format of either 'username:password' or 'username:hash:password'.") 330 | return (out_list) 331 | 332 | # Perform frequency analysis for [num] 333 | def check_frequency_analysis(full_list,length): 334 | z = 0 335 | pwd_list = [] 336 | words = Counter() 337 | total_pass_length = 0 338 | 339 | for pwd in full_list: 340 | x = pwd[1] 341 | if x == "": 342 | x = "*******BLANK-PASS*******" 343 | pwd_list.append(x) 344 | total_pass_length += 1 345 | 346 | words.update(pwd_list) 347 | wordfreq = (words.most_common()) 348 | 349 | for pair in wordfreq: 350 | if (z < length) and (args.output_report or args.summary): 351 | if int(pair[1] / int(len(full_list)) * 100) == 0: 352 | percent_out = ("< 1") 353 | else: 354 | percent_out = int(pair[1] / int(len(full_list)) * 100) 355 | print_report(str(pair[0]) + " : " + str(percent_out) + "%" + " | " + str(pair[1]) + "/" + str(total_pass_length)) 356 | z += 1 357 | elif z < length and args.output_report is False: 358 | output_pass(pair[0],str(pair[1]),"") 359 | z += 1 360 | 361 | # Perform frequency analysis for [num] 362 | def check_frequency_length(full_list,length): 363 | z = 0 364 | pwd_list = [] 365 | words = Counter() 366 | 367 | for pwd in full_list: 368 | x = pwd[1] 369 | pwd_list.append(len(x)) 370 | 371 | words.update(pwd_list) 372 | wordfreq = (words.most_common()) 373 | 374 | for pair in wordfreq: 375 | if (z < length) and (args.output_report or args.summary): 376 | 377 | if (int((pair[1] / len(pwd_list)) * 100)) == 0: 378 | percent_out = ("< 1") 379 | else: 380 | percent_out = str(int((pair[1] / len(pwd_list)) * 100)) 381 | 382 | print_report("Length : " + str(pair[0]) + " : " + percent_out + "%") 383 | z += 1 384 | elif z < length and args.output_report is False: 385 | output_pass(str(pair[0]),str(pair[1]),"") 386 | z += 1 387 | 388 | 389 | def password_masking(x): 390 | 391 | if x == "*******BLANK-PASS*******": 392 | mask_pwd = x 393 | else: 394 | # Output Masking 395 | if len(x) >= 9: 396 | mask_pwd = x[0:3] + ((len(x) - 6) * "*") + x[-3:] 397 | 398 | elif len(x) >= 5 and len(x) <= 8: 399 | mask_pwd = x[0:2] + ((len(x) - 4) * "*") + x[-2:] 400 | elif len(x) == 4: 401 | mask_pwd = x[0:1] + ((len(x) - 2) * "*") + x[-1:] 402 | elif len(x) == 3: 403 | mask_pwd = x[0:1] + ((len(x) - 2) * "*") + x[-1:] 404 | elif len(x) == 2: 405 | mask_pwd = x[0:1] + ((len(x) - 1) * "*") 406 | else: 407 | mask_pwd = x 408 | 409 | return mask_pwd 410 | 411 | def check_shared_pass(full_list): 412 | a = ([]) 413 | for item in full_list: 414 | a.append(item[1]) 415 | 416 | # Sort as collection 417 | y=collections.Counter(a) 418 | pwd_list = [i for i in y if y[i]>1] 419 | 420 | # Identifying duplicates and outputting 421 | for x in full_list: 422 | for z in pwd_list: 423 | if x[1] == z: 424 | #if ((len(x[1]) % 2) > 0): 425 | 426 | if args.output_report: 427 | print_report(str(x[0]) + " : " + password_masking(x[1])) 428 | else: 429 | output_pass(x[0],str(x[1]),"Password Re-Use") 430 | 431 | def check_reuse_pass(full_list,z): 432 | a = ([]) 433 | for item in full_list: 434 | a.append(item[1]) 435 | 436 | # Sort as collection 437 | #y=collections.Counter(a) 438 | #pwd_list = [i for i in y if y[i]>1] 439 | 440 | # Identifying duplicates and outputting 441 | for x in full_list: 442 | for user in full_list: 443 | if (user[0] != x[0]) and (user[0].lower() in x[0] and user[1] == x[1]): 444 | if args.output_report or z == 'summary': 445 | print_report(str(x[0]) + " : " + password_masking(x[1])) 446 | print_report(str(user[0]) + " : " + password_masking(user[1])) 447 | else: 448 | output_pass(x[0],str(x[1]),"Password Re-Use: " + x[0]) 449 | output_pass(user[0],str(user[1]),"Password Re-Use: " + user[0]) 450 | 451 | # reuse_pass 452 | 453 | # Run character analysis 454 | 455 | # Perform analysis analysis 456 | def check_character_analysis(full_list): 457 | z = 0 458 | pwd_list = [] 459 | words = Counter() 460 | upperList = [] 461 | lowerList = [] 462 | numList = [] 463 | specList = [] 464 | allList = [] 465 | 466 | for pwd in full_list: 467 | z += 1 468 | x = pwd[1] 469 | pwd_list.append(x) 470 | 471 | alphaCharList = ['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'] 472 | numCharList = ['0','1','2','3','4','5','6','7','8','9'] 473 | specCharList = ['!','@','£','#','$','%','^','&','*','(',')','-','+','=','_','|','?','`','±','§',';',':'] 474 | 475 | 476 | # At present I only report on top characters, but the *List dicts provide support for single charset type. 477 | for e in pwd_list: 478 | charLen = len(e) 479 | 480 | for char in alphaCharList: 481 | count = 0 482 | for i in range(charLen): 483 | if char.upper() == e[count]: 484 | upperList.append(e[count]) 485 | allList.append(e[count]) 486 | elif char.lower() == e[count]: 487 | lowerList.append(e[count]) 488 | allList.append(e[count]) 489 | count += 1 490 | 491 | for char in numCharList: 492 | count = 0 493 | for i in range(charLen): 494 | if char == e[count]: 495 | allList.append(e[count]) 496 | numList.append(e[count]) 497 | count += 1 498 | 499 | for char in specCharList: 500 | count = 0 501 | for i in range(charLen): 502 | if char == e[count]: 503 | specList.append(e[count]) 504 | allList.append(e[count]) 505 | count += 1 506 | 507 | words.update(allList) 508 | wordfreq = (words.most_common()) 509 | 510 | w = 0 511 | ast = "*" 512 | mostUsed = (wordfreq[0])[1] 513 | 514 | if args.output_report: 515 | print ("The top 20 characters used out of " + str(z) + " passwords:") 516 | 517 | for pair in wordfreq: 518 | if w != 20: 519 | percent = str((pair[1] / mostUsed) * 50).split('.')[0] 520 | if args.output_report: 521 | print_report(str((pair[0]) + " : " + str(ast * int(percent)) + str((10 - int(percent)) * " ") + "| " + str(pair[1]))) 522 | else: 523 | output_pass(str(pair[0]),str(pair[1]),"") 524 | w += 1 525 | print ("") 526 | 527 | def hashcat_mask_analysis(full_list): 528 | 529 | words = Counter() 530 | mask_list = [] 531 | 532 | for x in full_list: 533 | password = x[1] 534 | full_mask = "" 535 | mask = [] 536 | 537 | # Loop through each character in password string and regex against mask type 538 | for char in password: 539 | if re.match("[a-z]", char) is not None: 540 | mask.append("?l") 541 | elif re.match("[A-Z]", char) is not None: 542 | mask.append("?u") 543 | elif re.match("[0-9]", char) is not None: 544 | mask.append('?d') 545 | elif re.match("[!@£$%^&*()\[\]:;\\\/]", char) is not None: 546 | mask.append("?s") 547 | else: 548 | pass 549 | 550 | for z in mask: 551 | full_mask = full_mask + z 552 | 553 | if (len(full_mask) / 2) == len(password): 554 | mask_list.append(full_mask) 555 | 556 | words.update(mask_list) 557 | wordfreq = (words.most_common()) 558 | 559 | w = 0 560 | ast = "*" 561 | mostUsed = (wordfreq[0])[1] 562 | 563 | if args.output_report: 564 | print ("The top 10 Hashcat masks:") 565 | 566 | 567 | for m in wordfreq: 568 | if w != args.masks_results_count: # Limit output to 10 most common entries 569 | mask_length = str(int(len(m[0]) / 2)) 570 | mask_occurrence = str(m[1]) 571 | if args.output_report: 572 | print_report(m[0] + " - Length:" + mask_length + " - Occurrence: " + mask_occurrence) 573 | else: 574 | output_pass(str(m[0]),mask_length,mask_occurrence) 575 | w += 1 576 | print ("") 577 | 578 | 579 | def keyboard_patterns(full_list): 580 | keyboard_list = ["hjkl","asdf","lkjh","qwerty","qwer","zaqwsx","zaqxsw","qazwsx","qazxsw","zxc","zxcvbn","zxcdsa","1qaz","2wsx","poiuy","mnbvc","plm","nkoplm","qwer1234","2468","1357","3579","0864"] 581 | total_count = 0 582 | for x in full_list: 583 | count = 0 584 | for z in keyboard_list: 585 | if count > 0: 586 | continue 587 | if z.lower() in x[1].lower(): 588 | if args.output_report: 589 | print_report(str(x[0]) + " : " + password_masking(x[1])) 590 | count += 1 591 | elif args.summary: 592 | total_count += 1 593 | else: 594 | output_pass(x[0],str(x[1]),"Keyboard Pattern " + z.rstrip()) 595 | count += 1 596 | 597 | if args.summary: 598 | return (total_count) 599 | 600 | def remove_end_numeric(pass_list): 601 | f = open('wordlist-cleaned.txt','w') 602 | cleaned_pass_list = [] 603 | 604 | for p in pass_list: 605 | p = p[1] 606 | length = (len(p) - 1) 607 | last_alpha = length 608 | z = False 609 | 610 | break_check_length = len(p) 611 | 612 | if (len(p) == 0): 613 | continue 614 | 615 | if re.match("[a-zA-Z]",p[length]): 616 | cleaned_pass_list.append(p) 617 | z = True 618 | 619 | for n in range(0,length): 620 | temp_char = p[n] 621 | 622 | if re.match("[a-zA-Z]",temp_char): 623 | last_alpha = n + 1 624 | 625 | final_pass = p[:last_alpha] 626 | if z is False: 627 | cleaned_pass_list.append(final_pass) 628 | for pwd in cleaned_pass_list: 629 | f.write(pwd + '\n') 630 | f.close() 631 | 632 | def entropy_calculate(full_list): 633 | words = Counter() 634 | entropy_list = [] 635 | for x in full_list: 636 | temp_list = [] 637 | pwd = x[1] 638 | L = len(pwd) 639 | char_space = 0 640 | e = [] 641 | count = 0 642 | for c in pwd: 643 | if re.match('[a-z]',c): 644 | e.append(math.log2(26)) 645 | elif re.match('[A-Z]',c): 646 | e.append(math.log2(26)) 647 | elif re.match('[0-9]',c): 648 | e.append(math.log2(10)) 649 | else: 650 | e.append(math.log2(33)) 651 | # for n in pwd: 652 | # if re.match('[a-z]',n): 653 | # char_space += 26 654 | # elif re.match('[A-Z]',n): 655 | # char_space += 26 656 | # elif re.match('[0-9]',n): 657 | # char_space += 10 658 | # else: # 33 Special chars as per Hashcat !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ (including space) 659 | # char_space += 33 660 | #entropy = L * math.log2(char_space) 661 | 662 | for x in e: 663 | count += x 664 | entropy_list.append([int(count),pwd]) 665 | 666 | sorted_ent_list = sorted(entropy_list, reverse=True) 667 | if args.output_report: 668 | print ("\nThe following items are the top 10 estimated 'strongest' passwords (by entropy) that were able to be computed: ") 669 | else: 670 | output_pass("-" * 30,"-" * 30,"") 671 | output_pass("Entropy","Password","") 672 | output_pass("-" * 30,"-" * 30,"") 673 | 674 | w = 0 # Max output for top passwords 675 | for m in sorted_ent_list: 676 | 677 | pass_ent = m[0] 678 | if w != 10: # Limit output to 10 most common entries 679 | if args.output_report: 680 | print_report(str(pass_ent) + ' bits - ' + password_masking(m[1])) 681 | w += 1 682 | else: 683 | output_pass(str(pass_ent) + " bits",str(m[1]),"") 684 | w += 1 685 | 686 | sorted_ent_list = sorted(entropy_list, reverse=False) 687 | if args.output_report: 688 | print ("\nThe following items are the top 10 estimated 'weakest' passwords (by entropy) that were able to be computed: ") 689 | else: 690 | print ('\n') 691 | output_pass("-" * 30,"-" * 30,"") 692 | output_pass("Entropy","Password","") 693 | output_pass("-" * 30,"-" * 30,"") 694 | 695 | w = 0 # Max output for top passwords 696 | for m in sorted_ent_list: 697 | 698 | pass_ent = m[0] 699 | if w != 10: # Limit output to 10 most common entries 700 | if args.output_report: 701 | print_report(str(pass_ent) + ' bits - ' + password_masking(m[1])) 702 | w += 1 703 | else: 704 | output_pass(str(pass_ent) + " bits",str(m[1]),"") 705 | w += 1 706 | 707 | # Run main stuff 708 | if __name__ == "__main__": 709 | 710 | print (banner) 711 | 712 | update_check() 713 | 714 | if int(columns) < 110: 715 | sys.exit("[!] Warning: Resize your terminal to be at least 110 columns wide. Currently it is " + columns + " columns wide.") 716 | 717 | # Retrieve list 718 | full_list = (delimit_list(pass_list)) 719 | y = 0 720 | 721 | min_count = 0 722 | common_count = 0 723 | search_count = 0 724 | org_count = 0 725 | exact_count = 0 726 | admin_count = 0 727 | pass_count = 0 728 | date_day_count = 0 729 | shared_count = 0 730 | keyboard_count = 0 731 | reuse_count = 0 732 | 733 | if (args.output_report is False): 734 | if ((args.freq_anal is None) and (args.freq_len is None) and (args.masks is None)): 735 | if args.char_anal: 736 | output_pass("-" * 30,"-" * 30,"") 737 | output_pass("Character","Count","") 738 | output_pass("-" * 30,"-" * 30,"") 739 | 740 | elif (args.print_all is False): 741 | # Headers 742 | output_pass("-" * 30,"-" * 30,"-" * 30) 743 | output_pass("Username","Password","Description") 744 | output_pass("-" * 30,"-" * 30,"-" * 30) 745 | 746 | if args.freq_anal is not None: 747 | if args.output_report: 748 | print ("The following passwords were the " + str(args.freq_anal) + " most commonly used passwords that were able to be obtained:") 749 | check_frequency_analysis(full_list,args.freq_anal) 750 | else: 751 | output_pass("-" * 30,"-" * 30,"") 752 | output_pass("Password","Frequency","") 753 | output_pass("-" * 30,"-" * 30,"") 754 | 755 | check_frequency_analysis(full_list,args.freq_anal) 756 | 757 | if args.reuse_pass: 758 | if args.output_report: 759 | print ("\nThe following accounts were found to share the same password between similarly named accounts. These are believed to be service accounts or individual user accounts that are operated by the same user. Password re-use should be investigated as when a password is reused between a low privileged and a high privileged account it can lead to a compromise of systems that the high privileged account is authorised to access:") 760 | check_reuse_pass(full_list,'summary') 761 | else: 762 | output_pass("-" * 30,"-" * 30,"") 763 | output_pass("Username","Password","Description") 764 | output_pass("-" * 30,"-" * 30,"") 765 | check_reuse_pass(full_list,'') 766 | 767 | elif args.freq_len is not None: 768 | if args.output_report: 769 | print ("The following is a descending list of the most popular password lengths: ") 770 | check_frequency_length(full_list,args.freq_len) 771 | else: 772 | output_pass("-" * 30,"-" * 30,"") 773 | output_pass("Password Length","Frequency","") 774 | output_pass("-" * 30,"-" * 30,"") 775 | check_frequency_length(full_list,args.freq_len) 776 | 777 | 778 | 779 | elif args.char_anal: 780 | check_character_analysis(full_list) 781 | 782 | elif args.summary: 783 | print ("A password audit was performed against the extracted password hashes. Password cracking methods and tools were used to enumerate the plaintext password counterparts, and as such not all of the passwords were able to be identified. In total, there were " + str(len(full_list)) + " username and password combinations that were obtained and have been analysed.") 784 | 785 | # Top 10 most used passwords 786 | print ("\nAs part of the password audit, the top 10 most commonly used passwords within the organisation have been compiled. This list has been broken up with the password, the percentage of the total passwords, and the numeric value of the total passwords:") 787 | check_frequency_analysis(full_list,10) 788 | 789 | # Top 10 password lengths 790 | print ("\nAlongside the list of the most common passwords used within the organisation, the top 10 most common password lengths were analysed and the results can be seen below in the format of the character length and the percentage of the total passwords for each respective password length:") 791 | check_frequency_length(full_list,10) 792 | 793 | # Count of commonly seen passwords 794 | common_summary_count = 0 795 | for item in full_list: 796 | user = item[0] 797 | pwd = item[1] 798 | if pwd == "": 799 | pwd = "*******BLANK-PASS*******" 800 | if (check_common_pass(user,pwd)) == 1: 801 | common_summary_count += 1 802 | if (common_summary_count > 0): 803 | print ("\nOne of the biggest threats to organisations in relation to passwords used by users and administrators is the use of passwords that are the same, or a variation of commonly used passwords and phrases. Overall, there were " + str(common_summary_count) + " passwords that were found to have a variation of one of these common words or phrases. Some of these passwords are based on the words 'password', 'qwerty', 'starwars', 'system', 'admin', 'letmein', and 'iloveyou'. Further details can be seen within the 'pwd_common.conf' file at https://www.github.com/ins1gn1a/pwdlyser.") 804 | 805 | # Count of collated instances of shared passwords 806 | #check_shared_pass(full_list) 807 | 808 | # Count of keyboard patterns 809 | keyboard_summary_count = keyboard_patterns(full_list) 810 | if keyboard_summary_count > 0: 811 | print ("\nAs part of the wider password analysis, each password was assessed and compared to common keyboard patterns. These keyboard patterns were defined by the QWERTY layout, where a password is made up of characters in close proximity such as 'qwer', 'zxcvbn', and 'qazwsx' as an example. In total, there were " + str(keyboard_summary_count) + " passwords in use that had at least one of these or other variations.") 812 | 813 | # Count of username as password variation 814 | userpass_summary_count = 0 815 | for item in full_list: 816 | user = item[0] 817 | pwd = item[1] 818 | if pwd == "": 819 | pwd = "*******BLANK-PASS*******" 820 | 821 | if (check_user_as_pass(user,pwd)) == 1: 822 | userpass_summary_count += 1 823 | if (userpass_summary_count > 0): 824 | print ("\nThere were " + str(userpass_summary_count) + " passwords that were identified as having a password set that was a variation of the username; this includes additional prefixed or suffixed characters, substitutions within the word (i.e. 3 instead of e), or the username as it appears. Penetration testers, and more importantly attackers, will often check system or administrative accounts that have a variation of the username set as the password and as such it is critical that organisations do not use this convention for password security." ) 825 | 826 | # Count of organisation name in password 827 | # Requires -o parameter 828 | if args.org_name: 829 | org_summary_count = 0 830 | output_org_name = [] 831 | for item in full_list: 832 | user = item[0] 833 | pwd = item[1] 834 | if pwd == "": 835 | pwd = "*******BLANK-PASS*******" 836 | if (check_org_name(user,pwd,organisation)) is not None: 837 | output_org_name.append("- " + user + " : " + password_masking(pwd)) 838 | org_summary_count += 1 839 | if (org_summary_count) > 0: 840 | print ("\nThe organisation name, or a variation of the name (such as an abbreviation) '" + args.org_name + "' was found to appear within " + str(org_summary_count) + " of the passwords that were able to be obtained during the password audit. For any system or administrative user accounts that have a variation of the company name as their password, it is highly recommended that the passwords are changed to prevent targeted guessing attacks.") 841 | for u in output_org_name: 842 | print (u) 843 | 844 | print ("\nThe following accounts were found to share the same password between similarly named accounts. These are believed to be service accounts or individual user accounts that are operated by the same user. Password re-use should be investigated as when a password is reused between a low privileged and a high privileged account it can lead to a compromise of systems that the high privileged account is authorised to access:") 845 | check_reuse_pass(full_list,'summary') 846 | 847 | 848 | if args.admin_path: 849 | admin_summary_list = [] 850 | for item in full_list: 851 | user = item[0] 852 | pwd = item[1] 853 | if pwd == "": 854 | pwd = "*******BLANK-PASS*******" 855 | tmp_admin_summ_out = check_admin(user,pwd) 856 | if tmp_admin_summ_out is not None: 857 | admin_summary_list.append(check_admin(user,pwd)) 858 | 859 | if len(admin_summary_list) > 0: 860 | 861 | print ("\nFinally, there were " + str(len(admin_summary_list)) + " administrative accounts (based upon 'Domain Admins', 'Enterprise Admins', etc.) that were able to be compromised. The account names and their respective passwords (masked) can be seen below:") 862 | for admin_summary_pass in admin_summary_list: 863 | print (admin_summary_pass) 864 | 865 | else: 866 | # Print everything and exit 867 | if args.print_all: 868 | args.output_report = True 869 | 870 | check_character_analysis(full_list) 871 | 872 | print ("The following is a descending list of the most popular password lengths: ") 873 | check_frequency_length(full_list,10) 874 | print ("") 875 | 876 | print ("The following passwords were the 15 most commonly used passwords that were able to be obtained:") 877 | check_frequency_analysis(full_list,15) 878 | 879 | print ("\nThe length of the following user accounts have passwords set that do not meet the recommended minimum of 9 characters:") 880 | min_count = 9 881 | for item in full_list: 882 | user = item[0] 883 | pwd = item[1] 884 | if pwd == "": 885 | pwd = "*******BLANK-PASS*******" 886 | check_min_length(pwd,min_count) 887 | 888 | print ("\nThe following user accounts used a variation of the username as the password.") 889 | for item in full_list: 890 | user = item[0] 891 | pwd = item[1] 892 | if pwd == "": 893 | pwd = "*******BLANK-PASS*******" 894 | check_user_as_pass(user,pwd) 895 | 896 | if args.admin_path is not None: 897 | if args.output_report and admin_count == 0: 898 | print ("\nThe following user accounts were identified as Domain Administrators (Domain Admins, Enterprise Admins, Administrators, etc) and were found to have weak passwords set: ") 899 | admin_count += 1 900 | for item in full_list: 901 | user = item[0] 902 | pwd = item[1] 903 | if pwd == "": 904 | pwd = "*******BLANK-PASS*******" 905 | check_admin(user,pwd) 906 | 907 | if args.output_report and shared_count == 0: 908 | print ("\nThe following user accounts were found to have a passwords set that are re-used within other user accounts (with '*' representing a masked character). Usually, this is a coincidence with accounts using 'standard' weak password (such as 'Password1' or 'qwerty123', however where privileged/administrative accounts are used these should be reviewed further: ") 909 | shared_count += 1 910 | check_shared_pass(full_list) 911 | 912 | print ("\nThe following accounts were found to share the same password between similarly named accounts. These are believed to be service accounts or individual user accounts that are operated by the same user. Password re-use should be investigated as when a password is reused between a low privileged and a high privileged account it can lead to a compromise of systems that the high privileged account is authorised to access:") 913 | check_reuse_pass(full_list,'summary') 914 | 915 | 916 | print ("\nThe following user accounts were found to have a password that was a variation of a day or date (e.g. Monday01 or September2016):") 917 | for item in full_list: 918 | user = item[0] 919 | pwd = item[1] 920 | if pwd == "": 921 | pwd = "*******BLANK-PASS*******" 922 | check_date_day(user,pwd) 923 | 924 | # Count of organisation name in password 925 | # Requires -o parameter 926 | if args.org_name: 927 | org_summary_count = 0 928 | output_org_name = [] 929 | for item in full_list: 930 | user = item[0] 931 | pwd = item[1] 932 | if pwd == "": 933 | pwd = "*******BLANK-PASS*******" 934 | if (check_org_name(user,pwd,organisation)) is not None: 935 | output_org_name.append("- " + user + " : " + password_masking(pwd)) 936 | org_summary_count += 1 937 | if (org_summary_count) > 0: 938 | print ("\nThe organisation name, or a variation of the name (such as an abbreviation) '" + args.org_name + "' was found to appear within " + str(org_summary_count) + " of the passwords that were able to be obtained during the password audit. For any system or administrative user accounts that have a variation of the company name as their password, it is highly recommended that the passwords are changed to prevent targeted guessing attacks.") 939 | 940 | if args.output_report and common_count == 0: 941 | print ("\nThe following user accounts were found to have a password that was a variation of the most common user passwords, which can include 'password', 'letmein', '123456', 'admin', 'iloveyou', 'friday', or 'qwerty':") 942 | common_count += 1 943 | for item in full_list: 944 | user = item[0] 945 | pwd = item[1] 946 | if pwd == "": 947 | pwd = "*******BLANK-PASS*******" 948 | check_common_pass(user,pwd) 949 | 950 | if args.output_report and keyboard_count == 0: 951 | print ("\nThe following user accounts were identified as having passwords that utilise common keyboard patterns such as qwer, zxcvbn, qazwsx, etc.: ") 952 | keyboard_count += 1 953 | keyboard_patterns(full_list) 954 | 955 | sys.exit() # Skip analysis functions below 956 | 957 | 958 | # Check for passwords that don't meet Min Length 959 | if (args.min_length is not None): 960 | if args.output_report and min_count == 0: 961 | print ("\nThe length of the following user accounts have passwords set that do not meet the required minimum of " + str(args.min_length) + " characters:") 962 | min_count += 1 963 | for item in full_list: 964 | user = item[0] 965 | pwd = item[1] 966 | if pwd == "": 967 | pwd = "*******BLANK-PASS*******" 968 | check_min_length(pwd,args.min_length) 969 | 970 | # Count of organisation name in password 971 | # Requires -o parameter 972 | if args.org_name: 973 | org_summary_count = 0 974 | output_org_name = [] 975 | for item in full_list: 976 | user = item[0] 977 | pwd = item[1] 978 | if pwd == "": 979 | pwd = "*******BLANK-PASS*******" 980 | if (check_org_name(user,pwd,organisation)) is not None: 981 | output_org_name.append("- " + user + " : " + password_masking(pwd)) 982 | org_summary_count += 1 983 | if (org_summary_count) > 0: 984 | print ("\nThe organisation name, or a variation of the name (such as an abbreviation) '" + args.org_name + "' was found to appear within " + str(org_summary_count) + " of the passwords that were able to be obtained during the password audit. For any system or administrative user accounts that have a variation of the company name as their password, it is highly recommended that the passwords are changed to prevent targeted guessing attacks.") 985 | for u in output_org_name: 986 | print (u) 987 | 988 | # Check for passwords via unleeted search 989 | if args.basic_search is not None: 990 | if args.output_report and search_count == 0: 991 | print ("\nThe following user accounts were found to have a password that was some variation of the word/phrase: " + args.basic_search) 992 | search_count += 1 993 | for item in full_list: 994 | user = item[0] 995 | pwd = item[1] 996 | if pwd == "": 997 | pwd = "*******BLANK-PASS*******" 998 | check_basic_search(user,pwd,args.basic_search) 999 | 1000 | # Check for Common Passwords 1001 | if args.common_pass is True: 1002 | if args.output_report and common_count == 0: 1003 | print ("\nThe following user accounts were found to have a password that was a variation of the most common user passwords, which can include 'password', 'letmein', '123456', 'admin', 'iloveyou', 'friday', or 'qwerty':") 1004 | common_count += 1 1005 | for item in full_list: 1006 | user = item[0] 1007 | pwd = item[1] 1008 | if pwd == "": 1009 | pwd = "*******BLANK-PASS*******" 1010 | check_common_pass(user,pwd) 1011 | 1012 | # Check for Date/Day Passwords 1013 | if args.date_day is True: 1014 | if args.output_report and date_day_count == 0: 1015 | print ("\nThe following user accounts were found to have a password that was a variation of a day or date (e.g. Monday01 or September2016):") 1016 | date_day_count += 1 1017 | for item in full_list: 1018 | user = item[0] 1019 | pwd = item[1] 1020 | if pwd == "": 1021 | pwd = "*******BLANK-PASS*******" 1022 | check_date_day(user,pwd) 1023 | 1024 | # Search exact phrase or character 1025 | if args.exact_search is not None: 1026 | if args.output_report and exact_count == 0: 1027 | print ("\nThe following user accounts were found to have a password that contains the word/phrase " + args.exact_search + ":") 1028 | exact_count += 1 1029 | for item in full_list: 1030 | user = item[0] 1031 | pwd = item[1] 1032 | if pwd == "": 1033 | pwd = "*******BLANK-PASS*******" 1034 | check_exact_search(user,pwd,args.exact_search) 1035 | 1036 | # Check for username (basic search) 1037 | if args.user_search is not None: 1038 | for item in full_list: 1039 | user = item[0] 1040 | pwd = item[1] 1041 | check_user_search(user,pwd,args.user_search) 1042 | 1043 | # Check if admins have had their passwords cracked 1044 | if args.admin_path is not None: 1045 | if args.output_report and admin_count == 0: 1046 | print ("\nThe following user accounts were identified as Domain Administrators (Domain Admins, Enterprise Admins, Administrators, etc) and were found to have weak passwords set: ") 1047 | admin_count += 1 1048 | for item in full_list: 1049 | user = item[0] 1050 | pwd = item[1] 1051 | if pwd == "": 1052 | pwd = "*******BLANK-PASS*******" 1053 | check_admin(user,pwd) 1054 | 1055 | # Check if password contains username 1056 | if args.user_as_pass: 1057 | if args.output_report and pass_count == 0: 1058 | print ("\nThe following user accounts were found to have a variation of their username set as their account password: ") 1059 | pass_count += 1 1060 | for item in full_list: 1061 | user = item[0] 1062 | pwd = item[1] 1063 | if pwd == "": 1064 | pwd = "*******BLANK-PASS*******" 1065 | check_user_as_pass(user,pwd) 1066 | 1067 | # Check for password reuse between accounts 1068 | if args.shared_pass: 1069 | if args.output_report and shared_count == 0: 1070 | print ("\nThe following user accounts were found to have a passwords set that are re-used within other user accounts (with '*' representing a masked character). Usually, this is a coincidence with accounts using 'standard' weak password (such as 'Password1' or 'qwerty123', however where privileged/administrative accounts are used these should be reviewed further: ") 1071 | shared_count += 1 1072 | check_shared_pass(full_list) 1073 | 1074 | if args.keyboard_pattern: 1075 | if args.output_report and keyboard_count == 0: 1076 | print ("\nThe following user accounts were identified as having passwords that utilise common keyboard patterns such as qwer, zxcvbn, qazwsx, etc.: ") 1077 | keyboard_count += 1 1078 | keyboard_patterns(full_list) 1079 | 1080 | if args.masks: 1081 | if args.output_report is False: 1082 | output_pass("-" * 30,"-" * 30,"-" * 30) 1083 | output_pass("Hashcat Mask","Mask Length","Occurrences") 1084 | output_pass("-" * 30,"-" * 30,"-" * 30) 1085 | hashcat_mask_analysis(full_list) 1086 | 1087 | if args.clean_pass_wordlists: 1088 | print ("\n[*] Cleaned " + str(len(full_list)) + " words to 'wordlist-cleaned.txt'") 1089 | remove_end_numeric(full_list) 1090 | 1091 | if args.entropy: 1092 | entropy_calculate(full_list) 1093 | --------------------------------------------------------------------------------