├── .flake8 ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── customqueries.json ├── doc ├── bloodhound_clusters.png ├── radar.png └── txt_report.png ├── hashcathelper ├── __init__.py ├── __main__.py ├── _meta.py ├── analytics.py ├── args.py ├── bloodhound.py ├── consts.py ├── hashcat.py ├── log.py ├── md4.py ├── reporting.py ├── sql.py ├── subcommands │ ├── __init__.py │ ├── analytics.py │ ├── bloodhound.py │ ├── db.py │ └── ntlm.py ├── toggles-lm-ntlm.rule └── utils.py ├── pyproject.toml └── tests ├── OneRule.rule ├── OneRuleToRuleThemAll.rule ├── conftest.py ├── hash.txt ├── hash.txt.json ├── hash.txt.out ├── test_db.py ├── test_ntlm.py ├── test_utils.py └── words /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | show-source = True 3 | builtins = unicode 4 | max-line-length = 130 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_* 2 | hashcathelper.conf 3 | hashcathelper.pyz 4 | *.egg-info 5 | build 6 | dist 7 | __pycache__ 8 | .tox 9 | .venv 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.0.0] - 2023-11-15 10 | 11 | ### Added 12 | 13 | - Progress bar when creating BloodHound relationships 14 | - Add option for raw database query 15 | - Expose `--include-disabled` and `--include-computer-accounts` 16 | - Show cracked computer accounts in analytics report 17 | - Add option to `bloodhound` subcommand to mark users as cracked 18 | 19 | ### Changed 20 | 21 | - Changed the order of the key quantities in the analytics report to a more 22 | sensible one 23 | - Sort clusters in details section of analytics report by size 24 | - Change definition of a percentile from "less than" to "less than or equal" 25 | - Make port in the BloodHound URL format optional 26 | 27 | ### Fixed 28 | 29 | - Avoid printing passwords in log messages 30 | - HTML output of long tables like clusters 31 | 32 | ## [0.1.5] - 2022-08-30 33 | 34 | ### Added 35 | 36 | - A list of all found credentials can be included in the report with degree 37 | of detail >= 4 38 | - Support for encryption option in connections to the neo4j database 39 | 40 | ### Fixed 41 | 42 | - Percentage of 'user = password' was relative to cracked passwords, not all 43 | accounts 44 | 45 | ## [0.1.4] - 2022-06-27 46 | 47 | ### Added 48 | 49 | - Support `xlsx` format 50 | - Add parameter to change the minimum password length in the context of 51 | identifying short passwords 52 | - Include optional lookup in HIBP database in the detailed report 53 | - Add parameter `--keep-tempdir` 54 | - Add interactions with BloodHound: as a filter (#11) and adding 55 | "SamePassword" edges (#7) 56 | 57 | ### Changed 58 | 59 | - Performance improvement when creating the report 60 | - Require Python 3.6 or greater 61 | - Use MD4 implementation in pure Python because the openssl provider for 62 | `hashlib` may not support legacy algorithms 63 | - Improve error handling 64 | 65 | ## [0.1.3] - 2021-11-12 66 | 67 | ### Added 68 | 69 | - Support printing an entire single database entry 70 | - Support deleting single database entries 71 | - Support generating stats of reports without submitting them to the 72 | database first 73 | - Support HTML format in `analytics` subcommand 74 | - Add the `--degree-of-detail` switch in `analytics` subcommand 75 | - Add clusters to detailed report 76 | 77 | ### Changed 78 | 79 | - Catch CTRL-C during questionnaire when submitting data 80 | - Use `readline` so you can use backspace when interactively answering questions 81 | - Gracefully handle missing hashcat binary during questionnaire 82 | - Improve formatting of output of `db query` 83 | - User must now confirm before deleting an entry 84 | - Check hashcat's return code 85 | - Replace "empty" with "blank" in top 10 passwords list 86 | - Automatically remove computer accounts (end with $) or accounts that are 87 | marked as inactive in the pwdump file like `secretsdump -user-status` does 88 | it 89 | - Ignore hashcat warnings when retrieving usernames (#4) 90 | - Check existence of critical files before running `ntlm` subcommand 91 | - Handle `$HEX[]` passwords 92 | - Improve the way information is presented in the reports 93 | 94 | ### Fixed 95 | 96 | - "Higher is better" was applied twice when creating the stats 97 | - Prevent exception in `db stats` if there is no largest cluster 98 | - Prevent exception when creating a report and no top passwords exist 99 | - Fix LM detection when cracking several hash files at once 100 | - Handle files that contain single malformed lines 101 | 102 | ### Added 103 | 104 | - Show information about the number of entries when creating the stats 105 | 106 | ## [0.1.2] - 2021-08-19 107 | 108 | ### Added 109 | - Add usernames to wordlist 110 | 111 | ### Changed 112 | - Count nonunique passwords instead of unique passwords, since we want to 113 | minimize most metrics 114 | - Use more ordered dictionaries to get more consistency when using Python 115 | 3.5 116 | - Remove filtered accounts also from hashes 117 | - Skip LM hash cracking if they're all empty 118 | 119 | ### Fixed 120 | - Detection of the empty LM hash 121 | - Percentile computation on average password length (because more is better) 122 | - Formatting of percentages 123 | 124 | ### Removed 125 | - Attribute "empty_password" from analytics output 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SySS Research 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 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft hashcathelper 2 | prune tests 3 | prune docs 4 | prune .* 5 | exclude .* 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "install - install the package to the user's Python's site-packages" 3 | @echo "clean - remove all build, test, coverage and Python artifacts" 4 | @echo "lint - check style with flake8" 5 | @echo "test - run tests" 6 | @echo "deploy - package and upload a release" 7 | @echo "release - tag a new release and update changelog" 8 | @echo "build - create package" 9 | @echo "publish - upload package to PyPI" 10 | @echo "help - show this help and exit" 11 | 12 | deploy: hashcathelper.pyz 13 | @if [ -z $(SSH_TARGET) ] ; then echo "Environment variable SSH_TARGET is empty" ; return 0 ; else scp hashcathelper.pyz $(SSH_TARGET):.local/bin/hashcathelper ; fi 14 | 15 | PYTHON ?= python3 16 | 17 | hashcathelper.pyz: hashcathelper/ 18 | @$(eval TEMP_DIR := $(shell mktemp -d --suffix=.hashcathelper)) 19 | $(PYTHON) -m pip install . --upgrade --target "${TEMP_DIR}" 20 | @$(PYTHON) -m zipapp "${TEMP_DIR}" -m hashcathelper.__main__:main -p '/usr/bin/env $(PYTHON)' --output hashcathelper.pyz 21 | @rm -rf "${TEMP_DIR}" 22 | 23 | clean: 24 | @rm -rf build dist *.egg-info 25 | @find . -type f -name '*.pyc' -delete 26 | @find . -type d -name '__pycache__' | xargs rm -rf 27 | @rm -rf .tox 28 | @rm -f src/*.egg* 29 | @rm -f hashcathelper.pyz 30 | 31 | lint: 32 | @flake8 hashcathelper 33 | 34 | test: 35 | @rm -rf .tox 36 | @tox 37 | 38 | docs: 39 | @echo "Not yet implemented" 40 | 41 | install: 42 | python3 setup.py install --user 43 | 44 | # \n in sed only works in GNU sed 45 | release: 46 | @read -p "Enter version string (Format: x.y.z): " version; \ 47 | echo "Version Bump: $$version"; \ 48 | date=$$(date +%F); \ 49 | sed -i "s/^## \[Unreleased\]/## [Unreleased]\n\n## [$$version] - $$date/" CHANGELOG.md && \ 50 | git add CHANGELOG.md && \ 51 | git commit -m "Version bump: $$version" && \ 52 | read -p "Committed. Do you want to tag and push the new version? [y/n] " ans && \ 53 | if [ $$ans = 'y' ] ; then git tag $$version && git push && git push origin tag $$version && echo "Tagged and pushed." ; else echo "Tag it and push it yourself then." ; fi 54 | 55 | build: 56 | python -m build 57 | 58 | publish: 59 | @file=$$(ls -1t dist/hashcathelper-*.tar.gz | head -n1); \ 60 | read -p "Ready to upload $$file? Type yes: " ans; \ 61 | if [ $$ans = 'yes' ] ; then twine upload $$file ; fi 62 | 63 | .PHONY: build clean lint test docs deploy install help release publish 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hashcathelper 2 | ============= 3 | 4 | Convenience tool for hashcat. 5 | 6 | Usage 7 | ----- 8 | 9 | Run `hashcathelper -h` for help. The program is structured in subcommands. 10 | See `hashcathelper -h` for more information. 11 | 12 | ### Subcommand "ntlm" 13 | 14 | First, it bruteforces all LM hashes and uses the results to crack the 15 | corresponding NT hashes. Then, a large wordlist (recommendation: 16 | [Crackstation](https://crackstation.net/crackstation-wordlist-password-cracking-dictionary.htm)) 17 | is used together with a large ruleset (recommendation: 18 | [OneRule](https://notsosecure.com/one-rule-to-rule-them-all/)) to crack all 19 | remaining NT hashes. The list of account names is prepended to the wordlist, 20 | as hashcat does not automatically check if the account name is the password. 21 | 22 | The pwdump format is the one which is used by 23 | [secretsdump](https://github.com/SecureAuthCorp/impacket/blob/master/impacket/examples/secretsdump.py) 24 | or Meterpreter's 25 | [hashdump](https://www.offensive-security.com/metasploit-unleashed/meterpreter-basics/) 26 | function. 27 | 28 | Example: 29 | 30 | ``` 31 | $ hashcathelper ntlm dc01.ntds 32 | ``` 33 | 34 | ### Subcommand "analytics" 35 | 36 | Output interesting statistics about the cracked passwords. It is meant to be 37 | used together with the output of the `ntlm` subcommand, but passwords which 38 | were obtained elsewhere can be analyzed as well. 39 | 40 | It takes the following files as an input: 41 | 42 | * Password hashes in the pwdump format 43 | * Cracked passwords with accounts (output of the `ntlm` subcommand) 44 | * Plain passwords 45 | 46 | At least one of those is required. Ideally, you pass the hashes and the 47 | output of the `ntlm` subcommand. 48 | 49 | By default, computer accounts and accounts which are marked as `disabled` 50 | in the pwdump file (like `secretsdump -user-status` does) will be disregarded. 51 | 52 | Additionally, you can pass the path to a file containing account names to be 53 | used as a filter. Only the accounts whose names are listed in this file will 54 | be considered. This is useful if you are only interested in statistics 55 | regarding active accounts and did not use `secretsdump.py -user-status`, for 56 | example. Or you want the statistics regarding all accounts with `admin` in 57 | their name. Or statistics regarding kerberoastable users. 58 | 59 | In addition to a filter file, you can also pass a Cypher query (some are 60 | predefined) and the appropriate credentials to use information from a 61 | BloodHound database. 62 | 63 | Example: 64 | 65 | ``` 66 | $ hashcathelper analytics \ 67 | -H dc01.ntds \ 68 | -A dc01.ntds.out \ 69 | -F kerberoastable_accounts.txt \ 70 | -f text -o report.txt 71 | ``` 72 | 73 | The report comes as text, HTML, XLSX, or in JSON. The different sections contain 74 | different degrees of detail: 75 | 76 | * 1: Only show statistics 77 | * 2 (default): Show some password information such as top 10 lists 78 | * 3: Include full credentials of certain accounts, such as which accounts have blank passwords or clusters of accounts with the same passwords 79 | * 4: Include a full list of all credentials 80 | 81 | ![Example analytics report in text format](doc/txt_report.png) 82 | 83 | 84 | ### Subcommand "db" 85 | 86 | Use this subcommand to interact with the database. 87 | Results from the `analytics` subcommand can be submitted and collected in a 88 | database. This enables us to view statistics for each entry, for example how 89 | they compare to other customers. We can now make statements like this: 57% 90 | of all passwords could be cracked, which puts you in the bottom 20th 91 | percentile. 92 | 93 | Use `hashcathelper db submit ` to submit a result and `hashcathelper db 94 | stats ` to view statistics for one entry. 95 | 96 | ### Subcommand "bloodhound" 97 | 98 | This subcommand lets you insert new relationships into an existing 99 | [BloodHound](https://github.com/BloodHoundAD/BloodHound) database. It takes 100 | a BloodHound URI, a report in JSON format (with degree of detail equal to 101 | three or higher) and the domain name and creates edges between user objects 102 | that share the same password. This enables you to create graphs like this, 103 | which immediately shows you offenders of password reuse among the 104 | administrator team: 105 | 106 | ![Bloodhound showing clusters of tiered accounts](doc/bloodhound_clusters.png) 107 | 108 | This picture is the result of a query like this: 109 | 110 | ``` 111 | MATCH p=((a:User)-[r:SamePassword*1..2]-(b:User)) 112 | WHERE ALL(x in r WHERE STARTNODE(x).objectid > ENDNODE(x).objectid) 113 | AND ANY(c in [a,b] WHERE c.admincount OR c.name =~ '(?i)adm_.*') 114 | RETURN p 115 | ``` 116 | 117 | It might need some manual modification depending on the particular naming 118 | scheme for admin accounts. See `customqueries.json` for more queries. You 119 | can add these to `~/.config/bloodhound/customqueries.json`. 120 | 121 | Note that you can create reports with the `analytics` subcommand without 122 | having to actually crack anything; a JSON report can be created from just 123 | the hashes, which already enables us to see password reuse. 124 | 125 | Also, not all edges of a cluster are inserted, because the numbers of actual 126 | edges grows very quickly. Instead, one member of a cluster is chosen as the 127 | "center" and all other members have edges to this one member. So the 128 | property may not look transitive in BloodHound even though it is. Keep this 129 | in mind. 130 | 131 | ### Subcommand "autocrack" 132 | 133 | To be done; stay tuned. 134 | 135 | 136 | Installation 137 | ------------ 138 | 139 | The recommended way to install this package is to use `pipx` to pull it 140 | from PyPI: 141 | 142 | ``` 143 | $ pipx install hashcathelper 144 | ``` 145 | 146 | As with any other proper Python package, `pip` and virtual environments can 147 | be used as usual. 148 | 149 | Notes 150 | ----- 151 | 152 | ### Workflow 153 | 154 | The typical workflow starts with using secretsdump on a domain controller: 155 | 156 | ``` 157 | $ secretsdump.py /:@ -user-status -just-dc-ntlm -outputfile hashes.txt 158 | ``` 159 | 160 | This is passed to hashcathelper for cracking: 161 | 162 | ``` 163 | $ hashcathelper ntlm hashes.txt 164 | ``` 165 | 166 | Note that several files can be passed and cracked in parallel without it 167 | taking longer. 168 | 169 | Then, reports can be generated: 170 | 171 | ``` 172 | $ hashcathelper analytics -H hashes.txt -A hashes.txt.out -f json -o hashes.json 173 | ``` 174 | 175 | If secretsdump was run with `-user-status`, deactivated accounts are 176 | automatically disregarded. Computer accounts (those that end on `$`) are 177 | also disregarded. You can restrict analysis to a group of accounts by 178 | passing another file with `-F`. That file needs to contain one account name 179 | per line, without the UPN suffix (see below for more information). 180 | 181 | In the last step, you can submit the report to the database: 182 | 183 | ``` 184 | $ hashcathelper db submit hashes.json 185 | ``` 186 | 187 | If you have enough data, you can retrieve statistics about the data set: 188 | 189 | ``` 190 | $ hashcathelper db stats 191 | INFO - Connection to database: sqlite:////home/cracker/.local/share/hashcathelper/hch_db.sqlite 192 | The database holds information about 94037 accounts in 16 entries. 193 | Key Value Mean Std. Dev. Perc. 194 | ----------------------------------------------- ------- ------ ----------- ------- 195 | Accounts where password was cracked (%) 66.66 56.91 13.41 25 196 | Accounts with nonunique password (%) 46.11 23.09 11.78 0 197 | Accounts where username equals the password (%) 1.36 4.02 11.1 25 198 | Accounts with a non-empty LM hash (%) 3.19 8.42 16.76 50 199 | Accounts with an empty password (%) 0 1.17 2.87 50 200 | Largest baseword cluster (%) 45.2 10.22 10 0 201 | Average length of cracked passwords 8.39 9.58 0.84 6 202 | ``` 203 | 204 | The last column shows the percentile. It should be read as "this result is 205 | better than X% of all other results", so higher is better. These values can 206 | be visualized as radar chart using third-party tools: 207 | 208 | ![Example radar chart](doc/radar.png) 209 | 210 | 211 | ### UPN Suffix 212 | 213 | The output from secretsdump contains lines that start with the account name. The 214 | format looks like `\`, however, that is not the 215 | domain. It is the UPN suffix and can be entirely independent of the domain 216 | name -- it just coincides with the domain name by default. Especially after 217 | migrating an account from domain A to domain B, the UPN suffix will not 218 | change, but the domain name obviously will. 219 | 220 | Hashcathelper ignores the UPN suffix pretty much everywhere. All accounts in 221 | one file are assumed to belong to the same domain. And that is actually the 222 | case if the file has been created by using secretsdump on a domain 223 | controller -- unless you used the `-use-vss` flag, then there is a chance 224 | you might encounter duplicate entries. 225 | 226 | ### Config 227 | 228 | The config file (located at 229 | `${XDG_CONFIG_HOME:-$HOME/.config}/hashcathelper/hashcathelper.conf` or the CWD) should 230 | look like this: 231 | 232 | ``` 233 | [DEFAULT] 234 | 235 | # Path to hashcat binary 236 | hashcat_bin = /home/cracker/hashcat/hashcat-latest 237 | 238 | # Path to hashcat rule set (OneRule is recommended) 239 | rule = /home/cracker/hashcat/rules/OneRule.rule 240 | 241 | # Path to hashcat wordlist (Crackstation is recommended) 242 | wordlist = /home/cracker/wordlists/crackstation.txt 243 | 244 | # URI to database 245 | db_uri = sqlite:////home/cracker/.local/share/hashcathelper/stats.sqlite 246 | 247 | # Optional: Path to HIBP database 248 | # Must be a sorted list of NT hashes in upper case 249 | # Download here: https://haveibeenpwned.com/Passwords 250 | hibp_db = /home/cracker/wordlists/pwned-passwords-ntlm-ordered-by-hash-v8.txt 251 | ``` 252 | 253 | -------------------------------------------------------------------------------- /customqueries.json: -------------------------------------------------------------------------------- 1 | { 2 | "queries": [ 3 | { 4 | "name": "Show SamePassword clusters", 5 | "category": "Hashcathelper", 6 | "queryList": [ 7 | { 8 | "final": true, 9 | "query":"MATCH p=((a:User)-[r:SamePassword*1..2]-(b:User)) WHERE ALL(x in r WHERE STARTNODE(x).objectid > ENDNODE(x).objectid) AND a<>b RETURN p", 10 | "allowCollapse": true 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "Show SamePassword cluster for specific user", 16 | "category": "Hashcathelper", 17 | "queryList": [ 18 | { 19 | "final": false, 20 | "title": "Select user...", 21 | "query": 22 | "MATCH (n:User) RETURN n.name ORDER BY n.name ASC" 23 | }, 24 | { 25 | "final": true, 26 | "query":"MATCH p=((a:User {name: $result})-[r:SamePassword*1..2]-(b:User)) WHERE ALL(x in r WHERE STARTNODE(x).objectid > ENDNODE(x).objectid) AND a<>b RETURN p", 27 | "allowCollapse": true 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "Show SamePassword clusters of admins (adjust the regex in the Raw Query - needs Query Debug Mode)", 33 | "category": "Hashcathelper", 34 | "queryList": [ 35 | { 36 | "final": true, 37 | "query":"MATCH p=((a:User)-[r:SamePassword*1..2]-(b:User)) WHERE ALL(x in r WHERE STARTNODE(x).objectid > ENDNODE(x).objectid) AND (a.admincount OR a.name =~ '(?i)adm_.*') return p", 38 | "allowCollapse": true 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /doc/bloodhound_clusters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/hashcathelper/5f06804a56885c8a25b5c71c5e0498309304aa56/doc/bloodhound_clusters.png -------------------------------------------------------------------------------- /doc/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/hashcathelper/5f06804a56885c8a25b5c71c5e0498309304aa56/doc/radar.png -------------------------------------------------------------------------------- /doc/txt_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/hashcathelper/5f06804a56885c8a25b5c71c5e0498309304aa56/doc/txt_report.png -------------------------------------------------------------------------------- /hashcathelper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/hashcathelper/5f06804a56885c8a25b5c71c5e0498309304aa56/hashcathelper/__init__.py -------------------------------------------------------------------------------- /hashcathelper/__main__.py: -------------------------------------------------------------------------------- 1 | def main(argv=None): 2 | from hashcathelper.args import parse_args 3 | from hashcathelper.log import init_logging 4 | 5 | args = parse_args(argv=argv) 6 | init_logging(loglevel=args.log_level) 7 | args.func(args) 8 | 9 | 10 | if __name__ == "__main__": 11 | main() 12 | -------------------------------------------------------------------------------- /hashcathelper/_meta.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | __version__ = version("hashcathelper") 4 | __doc__ = "Convenience tool for hashcat" 5 | -------------------------------------------------------------------------------- /hashcathelper/analytics.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime as dt 2 | import collections 3 | import logging 4 | import re 5 | 6 | from hashcathelper.consts import NT_EMPTY, LM_EMPTY 7 | from hashcathelper.utils import User, get_nthash, line_binary_search 8 | from hashcathelper.reporting import ( 9 | Table, 10 | Report, 11 | Section, 12 | Histogram, 13 | RelativeQuantity, 14 | LongTable, 15 | List, 16 | ) 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | 21 | def median(lst): 22 | sortedLst = sorted(lst) 23 | lstLen = len(lst) 24 | index = (lstLen - 1) // 2 25 | 26 | if lstLen % 2: 27 | return sortedLst[index] 28 | else: 29 | return (sortedLst[index] + sortedLst[index + 1]) / 2.0 30 | 31 | 32 | def average(lst): 33 | return int(100 * sum(lst) / len(lst)) / 100 34 | 35 | 36 | def get_top_passwords(passwords, n=10): 37 | return Histogram( 38 | collections.OrderedDict( 39 | filter( 40 | lambda x: x[1] > 1, 41 | collections.Counter(passwords).most_common(n), 42 | ) 43 | ), 44 | "top10_passwords", 45 | ) 46 | 47 | 48 | def get_top_basewords(passwords, n=10): 49 | counts = collections.Counter() 50 | for p in passwords: 51 | if not p: 52 | continue 53 | # Convert to lower case 54 | p = p.lower() 55 | 56 | # Remove special chars and digits from beginning and end 57 | p = re.sub("[0-9!@#$%^&*()=_+~{}|\"? ><,./\\'[\\]-]*$", "", p) 58 | p = re.sub("^[0-9!@#$%^&*()=_+~{}|\" ?><,./\\'[\\]-]*", "", p) 59 | 60 | # De-leet-ify 61 | p = p.replace("!", "i") 62 | p = p.replace("1", "i") 63 | p = p.replace("0", "o") 64 | p = p.replace("3", "e") 65 | p = p.replace("4", "a") 66 | p = p.replace("@", "a") 67 | p = p.replace("+", "t") 68 | p = p.replace("$", "s") 69 | p = p.replace("5", "s") 70 | 71 | # Remove remaining special chars 72 | p = re.sub("[!@#$%^&*()=_+~{}|\"?><,./\\'[\\]-]", "", p) 73 | 74 | # Forget this if it's empty by now 75 | if not p: 76 | continue 77 | 78 | # Is it multiple words? Get the longest 79 | p = sorted(p.split(), key=len)[-1] 80 | 81 | # If there are digits left (i.e. it's not a word) or the word is 82 | # empty, we're not interested anymore 83 | if not re.search("[0-9]", p) and p: 84 | counts.update([p]) 85 | 86 | # Remove basewords shorter than 3 characters or occurance less than 2 87 | for k in counts.copy(): 88 | if counts[k] == 1 or len(k) < 3: 89 | del counts[k] 90 | top10 = collections.OrderedDict(counts.most_common(n)) 91 | return Histogram(top10, "top10_basewords") 92 | 93 | 94 | def get_char_classes(passwords): 95 | def get_character_classes(s): 96 | upper = False 97 | lower = False 98 | digits = False 99 | chars = False 100 | if re.search("[A-Z]", s): 101 | upper = True 102 | if re.search("[a-z]", s): 103 | lower = True 104 | if re.search("[0-9]", s): 105 | digits = True 106 | if re.search("[^A-Za-z0-9]", s): 107 | chars = True 108 | result = sum([upper, lower, digits, chars]) 109 | return result 110 | 111 | counts = collections.Counter() 112 | for p in passwords: 113 | classes = get_character_classes(p) 114 | counts.update([classes]) 115 | return counts 116 | 117 | 118 | def load_lines(path, as_user=True): 119 | """Load file and parse each line as `User()`""" 120 | if not path: 121 | return [] 122 | 123 | result = [] 124 | with open(path, "r", encoding="utf-8", errors="backslashreplace") as f: 125 | for i, line in enumerate(f.readlines()): 126 | if as_user: 127 | try: 128 | result.append(User(line)) 129 | except Exception as e: 130 | log.error("Error while parsing line %s:%d: %s" % (path, i, str(e))) 131 | else: 132 | result.append(line) 133 | return result 134 | 135 | 136 | def sort_dict(dct): 137 | return collections.OrderedDict(sorted(dct.items())) 138 | 139 | 140 | def do_sanity_check(hashes, accounts_plus_passwords, passwords, filter_accounts): 141 | """Make sure the right combination of files was passed""" 142 | if not (hashes or accounts_plus_passwords or passwords): 143 | log.error("No files specified, nothing to do") 144 | exit(1) 145 | 146 | if passwords and accounts_plus_passwords: 147 | log.warning( 148 | "accounts_plus_passwords specified, ignoring passwords file: " + passwords 149 | ) 150 | 151 | if filter_accounts and not (hashes or accounts_plus_passwords): 152 | log.warning( 153 | "filter_accounts specified, but not needed " 154 | "if neither hashes nor accounts_plus_passwords is given" 155 | ) 156 | 157 | 158 | def analyze_passwords(table, passwords): 159 | if "accounts" not in table: 160 | table["accounts"] = len(passwords) 161 | if "total_accounts" not in table: 162 | table["total_accounts"] = len(passwords) 163 | lengths = [len(p) for p in passwords] 164 | table["average_password_length"] = average(lengths) 165 | table["median_password_length"] = median(lengths) 166 | password_length_count = Histogram( 167 | sort_dict(collections.Counter(lengths)), 168 | "password_length_count", 169 | ) 170 | char_classes = get_char_classes(passwords) 171 | char_class_count = Histogram(sort_dict(char_classes), "char_class_count") 172 | table["average_character_classes"] = ( 173 | int(sum(k * v for k, v in char_classes.items()) / len(passwords) * 100) / 100 174 | ) 175 | 176 | return password_length_count, char_class_count 177 | 178 | 179 | def count_user_equal_password(table, accounts_plus_passwords, total): 180 | count = 0 181 | for u in accounts_plus_passwords: 182 | if u == u.password: 183 | count += 1 184 | table["user_equals_password"] = RelativeQuantity(count, total) 185 | 186 | 187 | def analyze_hashes(table, hashes, passwords): 188 | table["accounts"] = len(hashes) 189 | if "total_accounts" not in table: 190 | table["total_accounts"] = len(hashes) 191 | if passwords: 192 | table["cracked"] = RelativeQuantity(len(passwords), len(hashes)) 193 | lm_hash_count = 0 194 | computer_acc_count = 0 195 | for u in hashes: 196 | if u.lmhash != LM_EMPTY: 197 | lm_hash_count += 1 198 | if u.is_computer_account(): 199 | computer_acc_count += 1 200 | 201 | if computer_acc_count: 202 | log.warning( 203 | "%d computer accounts found in hash file. You should remove these." 204 | % computer_acc_count 205 | ) 206 | 207 | table["lm_hash_count"] = RelativeQuantity(lm_hash_count, table["accounts"]) 208 | 209 | 210 | def remove_accounts(table, accounts_plus_passwords, hashes, remove=[], keep_only=[]): 211 | """Remove all lines from `hashes` and `accounts_plus_passwords` which 212 | have a username which is either specified in `remove` or not specified 213 | in `keep_only` (if `keep_only` is non-empty). 214 | 215 | Accounts are assumed to be case-insensitive. The UPN suffix (e.g. the 216 | domain name) is ignored if there is one. 217 | 218 | `table` must be a suitable dictionary, the other args lists of `User()`. 219 | """ 220 | remove_set = set(remove) 221 | keep_set = set(keep_only) 222 | # Remove entries from first list 223 | if accounts_plus_passwords: 224 | before = len(accounts_plus_passwords) 225 | accounts_plus_passwords = [ 226 | u for u in accounts_plus_passwords if u not in remove_set 227 | ] 228 | if keep_only: 229 | accounts_plus_passwords = [ 230 | u for u in accounts_plus_passwords if u in keep_set 231 | ] 232 | after = len(accounts_plus_passwords) 233 | table["total_accounts"] = before 234 | table["removed"] = before - after 235 | 236 | # Remove entries from second list 237 | if hashes: 238 | before = len(hashes) 239 | hashes = [u for u in hashes if u not in remove_set] 240 | if keep_only: 241 | hashes = [u for u in hashes if u in keep_set] 242 | after = len(hashes) 243 | table["total_accounts"] = before 244 | table["removed"] = before - after 245 | 246 | return accounts_plus_passwords, hashes 247 | 248 | 249 | def get_hibp(hashes, hibp_db): 250 | """Look up how many hashes are in the HIBP database 251 | 252 | HIBP stands for Have I been Pwned. The HIBP database must be a flat, 253 | sorted text file of NT hashes in the format `:`. 255 | 256 | Arguments: 257 | hashes: a list of `User` objects 258 | hibp_db: path to the HIBP database 259 | 260 | Returns: 261 | A list of affected usernames 262 | """ 263 | 264 | pos = 0 265 | result = [] 266 | hashes.sort(key=lambda x: x.nthash) 267 | for u in hashes: 268 | h = u.nthash.upper().encode() 269 | results, new_pos = line_binary_search( 270 | hibp_db, 271 | h, 272 | lambda line: line[:32], 273 | start=pos, 274 | ) 275 | if results: 276 | pos = new_pos 277 | result.append(u.username) 278 | else: 279 | pass 280 | return List("hibp_accounts", result) 281 | 282 | 283 | def create_report( 284 | hashes=None, 285 | accounts_plus_passwords=None, 286 | passwords=None, 287 | filter_accounts=[], 288 | pw_min_length=6, 289 | degree_of_detail=1, 290 | include_disabled=False, 291 | include_computer_accounts=False, 292 | hibp_db=None, 293 | ): 294 | """Create the report on password statistics 295 | 296 | Arguments: 297 | hashes: path to the output file from secretsdump or similar 298 | accounts_plus_passwords: path to the output file of hashcat with 299 | user names 300 | passwords: path to a file containing only passwords (one per line) 301 | filter_accounts: list of `User` objects, which will be the only ones 302 | considered 303 | pw_min_length: definition of a password that is 'too short' 304 | degree_of_detail (int): amount of detail to include 305 | include_disabled: don't remove disabled accounts 306 | include_computer_accounts: don't remove computer accounts 307 | hibp_db: path to the HIBP database (sorted by NT hash) 308 | """ 309 | log.info("Creating report...") 310 | table = Table("key_quantities", collections.OrderedDict()) 311 | 312 | do_sanity_check(hashes, accounts_plus_passwords, passwords, filter_accounts) 313 | meta = Table( 314 | "meta", 315 | collections.OrderedDict( 316 | filename_hashes=hashes, 317 | filename_result=accounts_plus_passwords, 318 | filename_passwords=passwords, 319 | filename_filter=filter_accounts, 320 | timestamp=str(dt.now()), 321 | ), 322 | formats=["json"], 323 | ) 324 | 325 | # Load data from files 326 | hashes = load_lines(hashes) 327 | accounts_plus_passwords = load_lines(accounts_plus_passwords) 328 | passwords = load_lines(passwords, as_user=False) 329 | 330 | # Remove computer accounts and accounts marked by hashcat as 'disabled' 331 | disabled = [] 332 | computer_accounts = [] 333 | if not include_disabled or not include_computer_accounts: 334 | for u in hashes: 335 | if u.is_disabled() and not include_disabled: 336 | disabled.append(u) 337 | if u.is_computer_account() and not include_computer_accounts: 338 | computer_accounts.append(u) 339 | 340 | cracked_computer_accounts = set(computer_accounts).intersection( 341 | accounts_plus_passwords 342 | ) 343 | 344 | # Filter accounts 345 | log.debug("Filter accounts") 346 | if filter_accounts: 347 | log.info( 348 | "Removing all accounts which are not in filter (%d)" % len(filter_accounts) 349 | ) 350 | if disabled: 351 | log.info( 352 | "Removing %d accounts which have been marked as disabled" % len(disabled) 353 | ) 354 | if computer_accounts: 355 | log.info("Removing %d computer accounts" % len(computer_accounts)) 356 | 357 | accounts_plus_passwords, hashes = remove_accounts( 358 | table, 359 | accounts_plus_passwords, 360 | hashes, 361 | remove=disabled + computer_accounts, 362 | keep_only=filter_accounts, 363 | ) 364 | if table["removed"] == 0: 365 | log.warning( 366 | "No accounts filtered. Are you sure?" 367 | " At least inactive accounts should be filtered." 368 | ) 369 | log.debug("Removed %d accounts" % table["removed"]) 370 | 371 | # Count cracked computer accounts 372 | table["cracked_computer_accounts"] = len(cracked_computer_accounts) 373 | 374 | # Count accounts where user==password 375 | if accounts_plus_passwords: 376 | count_user_equal_password(table, accounts_plus_passwords, len(hashes)) 377 | 378 | # Remove account names now that they are filtered 379 | if not passwords and accounts_plus_passwords: 380 | passwords = [u.password for u in accounts_plus_passwords] 381 | 382 | # Analyze hashes only 383 | log.debug("Analyze hashes") 384 | if hashes: 385 | analyze_hashes(table, hashes, passwords) 386 | nt_hashes = [u.nthash for u in hashes] 387 | clusters = cluster_analysis(table, nt_hashes, empty=NT_EMPTY) 388 | 389 | # Analyze passwords 390 | log.debug("Analyze passwords") 391 | if passwords: 392 | password_length_count, char_class_count = analyze_passwords(table, passwords) 393 | if not hashes: 394 | clusters = cluster_analysis(table, passwords, empty="") 395 | else: 396 | password_length_count, char_class_count = None, None 397 | 398 | sort_table(table) 399 | 400 | result = Report("report") 401 | result += meta 402 | 403 | if degree_of_detail > 0: 404 | statistics = Section("statistics") 405 | statistics += table 406 | statistics += clusters 407 | if password_length_count: 408 | statistics += password_length_count 409 | if char_class_count: 410 | statistics += char_class_count 411 | result += statistics 412 | 413 | if degree_of_detail > 1: 414 | log.debug("Get top passwords") 415 | s = Section("sensitive_data") 416 | s += get_top_passwords(passwords) 417 | s += get_top_basewords(passwords) 418 | result += s 419 | 420 | if degree_of_detail > 2: 421 | # Add details: accounts with short passwords; clusters 422 | details = gather_details( 423 | hashes, 424 | accounts_plus_passwords, 425 | pw_min_length, 426 | hibp_db, 427 | ) 428 | details += List( 429 | "cracked_computer_accounts", 430 | cracked_computer_accounts, 431 | ) 432 | result += details 433 | 434 | if degree_of_detail > 3: 435 | # Add all known credentials 436 | creds = gather_creds( 437 | hashes, 438 | accounts_plus_passwords, 439 | ) 440 | result += creds 441 | 442 | return result 443 | 444 | 445 | def gather_creds(hashes, accounts_plus_passwords): 446 | """Return a dictionary with credentials""" 447 | creds = LongTable( 448 | "full_creds", 449 | dict( 450 | sorted({u.username: [u.password] for u in accounts_plus_passwords}.items()) 451 | ), 452 | ) 453 | return creds 454 | 455 | 456 | def gather_details(hashes, accounts_plus_passwords, pw_min_length, hibp_db): 457 | """Return a dictionary with details about the report 458 | 459 | Contains: 460 | * list of accounts with short passwords 461 | * list of accounts where usename equals password (case insensitive) 462 | * list of accounts where usename is similar to password (starts or 463 | ends with password, case insensitive) 464 | * list of clusters; either based on hash or based on password if 465 | cracked 466 | """ 467 | short_password = LongTable( 468 | "short_password", 469 | collections.OrderedDict((i, []) for i in range(pw_min_length)), 470 | ) 471 | user_equals_password = List("user_equals_password", []) 472 | user_similarto_password = List("user_similarto_password", []) 473 | 474 | for u in accounts_plus_passwords: 475 | if len(u.password) < pw_min_length: 476 | short_password[len(u.password)].append(u.username) 477 | if u.username.lower() == u.password.lower(): 478 | user_equals_password.append(u.username) 479 | elif u.password and ( 480 | u.username.lower() in u.password.lower() 481 | or u.password.lower() in u.username.lower() 482 | ): 483 | user_similarto_password.append(u.username) 484 | 485 | # Find clusters 486 | clusters = collections.defaultdict(list) 487 | for u in hashes: 488 | clusters[u.nthash].append(u.username) 489 | 490 | # Remove non-clusters 491 | for h in list(clusters.keys()): 492 | if len(clusters[h]) == 1: 493 | del clusters[h] 494 | 495 | # Replace hashes with passwords where possible 496 | # Build dict of nthash->password to avoid n^2 loop 497 | hash_map = { 498 | get_nthash(u.password.encode()): u.password for u in accounts_plus_passwords 499 | } 500 | 501 | for h in list(clusters.keys()): 502 | if h in hash_map: 503 | clusters[hash_map[h]] = clusters[h] 504 | del clusters[h] 505 | 506 | # Sort clusters by size 507 | clusters = collections.OrderedDict( 508 | sorted([(k, v) for k, v in clusters.items()], key=lambda x: -len(x[1])) 509 | ) 510 | 511 | clusters = LongTable("clusters", clusters) 512 | 513 | # Build section 514 | details = Section("details") 515 | details += clusters 516 | details += user_equals_password 517 | details += user_similarto_password 518 | details += short_password 519 | if hibp_db: 520 | try: 521 | details += get_hibp(hashes, hibp_db) 522 | except FileNotFoundError: 523 | log.error("Could not include HIBP stats; file not found: %s" % hibp_db) 524 | else: 525 | log.error("No HIBP database defined; skipping this detail") 526 | return details 527 | 528 | 529 | def cluster_analysis(table, values, empty=""): 530 | counter = collections.Counter(values) 531 | clusters = dict(c for c in counter.most_common() if c[1] > 1) 532 | cluster_count = Histogram( 533 | sort_dict(collections.Counter(c for c in clusters.values() if c > 1)), 534 | "cluster_count", 535 | ) 536 | 537 | if "accounts" in table: 538 | total = table["accounts"] 539 | else: 540 | total = len(values) 541 | 542 | nonunique = sum(count for _, count in counter.items() if count > 1) 543 | table["nonunique"] = RelativeQuantity(nonunique, total) 544 | 545 | table["empty_password"] = RelativeQuantity(counter[empty], total) 546 | return cluster_count 547 | 548 | 549 | def sort_table(table): 550 | """Sort entries of table like the labels""" 551 | from hashcathelper.consts import labels 552 | 553 | result = collections.OrderedDict() 554 | for k in labels.keys(): 555 | if k in table: 556 | result[k] = table[k] 557 | del table[k] 558 | table_copy = collections.OrderedDict(table) 559 | for k, v in table_copy.items(): 560 | result[k] = v 561 | del table[k] 562 | table.update(result) 563 | 564 | 565 | def create_short_report( 566 | submitter_email, 567 | wordlist, 568 | rule_set, 569 | hashcat_version, 570 | data, 571 | ): 572 | """Produce a dictionary that can be submitted to the DB""" 573 | 574 | from datetime import datetime as dt 575 | from hashcathelper._meta import __version__ 576 | 577 | try: 578 | timestamp = data["meta"]["timestamp"] 579 | cracking_date = dt.strptime(timestamp, "%Y-%m-%d %H:%M:%S.%f") 580 | except (KeyError, ValueError): 581 | log.error("Failed to parse cracking date") 582 | cracking_date = None 583 | 584 | key_quantities = data["statistics"]["key_quantities"] 585 | 586 | def get_value(item): 587 | # for values with percentage 588 | val = key_quantities[item] 589 | if isinstance(val, list) and len(val) == 2: 590 | return val[0] 591 | return val 592 | 593 | top_basewords = data["sensitive_data"]["top10_basewords"].values() 594 | if top_basewords: 595 | largest_cluster = max(top_basewords) 596 | else: 597 | largest_cluster = 0 598 | r = dict( 599 | submitter_email=submitter_email, 600 | submission_date=dt.now(), 601 | cracking_date=cracking_date, 602 | wordlist=wordlist, 603 | rule_set=rule_set, 604 | hashcathelper_version=__version__, 605 | hashcat_version=hashcat_version, 606 | accounts=key_quantities["accounts"], 607 | cracked=get_value("cracked"), 608 | nonunique=get_value("nonunique"), 609 | user_equals_password=get_value("user_equals_password"), 610 | lm_hash_count=get_value("lm_hash_count"), 611 | empty_password=get_value("empty_password"), 612 | average_password_length=key_quantities["average_password_length"], 613 | largest_baseword_cluster=largest_cluster, 614 | ) 615 | return r 616 | -------------------------------------------------------------------------------- /hashcathelper/args.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pkgutil 3 | from importlib import import_module 4 | import logging 5 | 6 | from hashcathelper._meta import __version__, __doc__ 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | parser = argparse.ArgumentParser( 11 | description=__doc__, 12 | ) 13 | 14 | parser.add_argument( 15 | "-v", 16 | "--version", 17 | action="version", 18 | version="hashcathelper %s" % __version__, 19 | ) 20 | 21 | parser.add_argument( 22 | "-c", 23 | "--config", 24 | type=str, 25 | help="path to config file; if empty we will try ./hashcathelper.conf" 26 | " and ${XDG_CONFIG_HOME:-$HOME/.config}/hashcathelper/hashcathelper.conf" 27 | " in that order", 28 | ) 29 | 30 | parser.add_argument( 31 | "-l", 32 | "--log-level", 33 | choices=["INFO", "WARNING", "ERROR", "DEBUG"], 34 | default="INFO", 35 | help="log level (default: %(default)s)", 36 | ) 37 | 38 | 39 | subparsers = parser.add_subparsers(help="choose a sub-command", dest="subcommand") 40 | # Keep track of the subparsers we add so we can add subsubparsers 41 | subparsers_map = {} 42 | 43 | 44 | def argument(*name_or_flags, **kwargs): 45 | """Convenience function to properly format arguments to pass to the 46 | subcommand decorator. 47 | """ 48 | return (list(name_or_flags), kwargs) 49 | 50 | 51 | def subcommand(args=[], parent=subparsers): 52 | """Decorator to define a new subcommand in a sanity-preserving way. 53 | The function will be stored in the ``func`` variable when the parser 54 | parses arguments so that it can be called directly like so:: 55 | args = cli.parse_args() 56 | args.func(args) 57 | Usage example:: 58 | @subcommand([argument("-d", help="Enable debug mode", 59 | action="store_true")]) 60 | def subcommand(args): 61 | print(args) 62 | Then on the command line:: 63 | $ python cli.py subcommand -d 64 | """ 65 | 66 | def decorator(func): 67 | parser = parent.add_parser(func.__name__, description=func.__doc__) 68 | for arg in args: 69 | parser.add_argument(*arg[0], **arg[1]) 70 | parser.set_defaults(func=func) 71 | subparsers_map[func.__name__] = parser 72 | 73 | return decorator 74 | 75 | 76 | def parse_args(argv=None): 77 | from hashcathelper import subcommands 78 | 79 | for importer, modname, _ in pkgutil.iter_modules(subcommands.__path__): 80 | import_module("..subcommands." + modname, __name__) 81 | args = parser.parse_args(argv) 82 | if not args.subcommand: 83 | parser.print_help() 84 | exit(0) 85 | return args 86 | 87 | 88 | def parse_config(path): 89 | import configparser 90 | import collections 91 | import os 92 | 93 | import xdg.BaseDirectory 94 | 95 | config_parser = configparser.ConfigParser() 96 | if not path: 97 | path = "./hashcathelper.conf" 98 | if not os.path.exists(path): 99 | path = os.path.join( 100 | xdg.BaseDirectory.xdg_config_home, 101 | "hashcathelper", 102 | "hashcathelper.conf", 103 | ) 104 | config_parser.read(path) 105 | attrs = "rule wordlist hashcat_bin hash_speed db_uri hibp_db".split() 106 | for a in attrs: 107 | if a not in config_parser["DEFAULT"]: 108 | log.error("Attribute undefined: " + a) 109 | Config = collections.namedtuple("Config", attrs) 110 | config = Config(*[config_parser["DEFAULT"].get(a) for a in attrs]) 111 | 112 | return config 113 | -------------------------------------------------------------------------------- /hashcathelper/bloodhound.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from neo4j import GraphDatabase 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | 8 | CYPHER_QUERIES = { 9 | "enabled": "MATCH (u:User) WHERE u.enabled=true RETURN u", 10 | "kerberoastable": """ 11 | MATCH (u:User)WHERE u.hasspn=true and u.enabled=true RETURN u 12 | """, 13 | "admincount": """ 14 | MATCH (u:User)WHERE u.admincount=true AND u.enabled=true RETURN u 15 | """, 16 | "localadmins": """ 17 | MATCH p = (u:User)-[:MemberOf|AdminTo*1..5]->(C:Computer) 18 | WHERE u.enabled=true RETURN DISTINCT u 19 | """, 20 | "domainadmins": """ 21 | MATCH p = (u:User)-[:MemberOf*1..5]->(g:Group) 22 | WHERE g.objectid =~ '(?i)S-1-5-.*-512' AND u.enabled=true 23 | RETURN DISTINCT u 24 | """, 25 | # "effective-domainadmins": "", 26 | } 27 | 28 | 29 | def get_driver(url): 30 | import re 31 | 32 | if not url: 33 | log.critical("No BloodHound URL given") 34 | exit(1) 35 | regex = r"^bolt(?Ps?)://(?P[^:]+):(?P.+)@" 36 | regex += r"(?P[^:]*)(:(?P[0-9]+))?$" 37 | m = re.match(regex, url) 38 | if not m: 39 | log.error("Couldn't parse BloodHound URL: %s" % url) 40 | exit(1) 41 | 42 | encrypted, user, password, host, _, port = m.groups() 43 | encrypted = encrypted == "s" 44 | 45 | url = "bolt://%s:%s" % (host, port or 7687) 46 | 47 | log.debug("Connecting to %s..." % url) 48 | driver = GraphDatabase.driver(url, auth=(user, password), encrypted=encrypted) 49 | 50 | return driver 51 | 52 | 53 | def query_neo4j(driver, cypher_query, domain=None): 54 | """Query the neo4j for users 55 | 56 | If given, filter for domain and return `User()` objects. 57 | """ 58 | from hashcathelper.utils import User 59 | 60 | log.debug("Given Cypher query: %s" % cypher_query) 61 | log.info("Querying BloodHound database...") 62 | 63 | q = CYPHER_QUERIES.get(cypher_query, cypher_query) 64 | result = [] 65 | with driver.session() as session: 66 | for x in session.run(q).value(): 67 | # if domain is specified, apply as a filter 68 | if not domain or x["domain"].lower() == domain.lower(): 69 | u = User(x["name"]) 70 | result.append(u) 71 | 72 | log.debug("Query result: %s" % result) 73 | return result 74 | 75 | 76 | def add_edges(driver, clusters): 77 | from tqdm import tqdm 78 | 79 | rel_count = 0 80 | log.info("Processing %d clusters..." % len(clusters)) 81 | with driver.session() as session: 82 | for cluster in tqdm(clusters): 83 | # Only create clusters instead of cliques. In cliques, the 84 | # number of edges grows as n^2, which can become overwhelming 85 | # and doesn't add much value. 86 | if len(cluster) <= 1: 87 | continue 88 | edges = [] 89 | node_a = cluster[0] 90 | for node_b in cluster[1:]: 91 | edges.append( 92 | { 93 | "a": node_a, 94 | "b": node_b, 95 | } 96 | ) 97 | added = session.write_transaction(add_many_edges, edges) 98 | rel_count += added 99 | log.info("Added %d relationships to BloodHound" % rel_count) 100 | 101 | 102 | def add_many_edges(tx, edges): 103 | q = """ 104 | UNWIND $edges as edge 105 | MATCH (a:User), (b:User) 106 | WHERE a.name = toUpper(edge.a) 107 | AND b.name = toUpper(edge.b) 108 | CREATE (a)-[r:SamePassword]->(b), (b)-[k:SamePassword]->(a) 109 | RETURN r 110 | """ 111 | result = tx.run(q, edges=edges) 112 | return len(result.value()) 113 | 114 | 115 | def mark_cracked(driver, users): 116 | """Set the attribute `cracked=True` on a list of users""" 117 | added = 0 118 | with driver.session() as session: 119 | added = session.write_transaction(mark_cracked_tx, users) 120 | 121 | log.info("Marked %d users as 'cracked'" % added) 122 | 123 | 124 | def mark_cracked_tx(tx, users): 125 | q = """ 126 | UNWIND $users as user 127 | MATCH (u:User {name: user}) 128 | SET u.cracked = True 129 | RETURN u 130 | """ 131 | result = tx.run(q, users=users) 132 | return len(result.value()) 133 | -------------------------------------------------------------------------------- /hashcathelper/consts.py: -------------------------------------------------------------------------------- 1 | labels = dict( 2 | total_accounts="Total number of accounts", 3 | removed="Accounts removed from analysis", 4 | accounts="Accounts subject to analysis", 5 | cracked="Accounts where password was cracked", 6 | cracked_computer_accounts="Computer accounts where password was cracked", 7 | lm_hash_count="Accounts with a non-empty LM hash", 8 | nonunique="Accounts with nonunique password", 9 | empty_password="Accounts with a blank password", 10 | average_password_length="Average length of cracked passwords", 11 | median_password_length="Median length of cracked passwords", 12 | top10_passwords="Top 10 Passwords", 13 | top10_basewords="Top 10 Basewords", 14 | char_class_count="Character classes", 15 | average_character_classes="Average number of character classes of" 16 | " cracked passwords", 17 | password_length_count="Lengths of cracked passwords", 18 | cluster_count="Cluster sizes", 19 | user_equals_password="Accounts where username equals the password", 20 | largest_baseword_cluster="Largest baseword cluster", 21 | report="Report", 22 | statistics="Statistics", 23 | meta="Meta information", 24 | key_quantities="Key quantities", 25 | sensitive_data="Sensitive data", 26 | details="Details", 27 | clusters="Clusters", 28 | user_similarto_password="Accounts where username is similar to password", 29 | short_password="Accounts with short passwords", 30 | full_creds="Full credentials", 31 | hibp_accounts="Accounts whose hash is known to HIBP", 32 | ) 33 | 34 | 35 | LM_EMPTY = "aad3b435b51404eeaad3b435b51404ee" 36 | NT_EMPTY = "31d6cfe0d16ae931b73c59d7e0c089c0" 37 | 38 | # Format: (name, speed factor) 39 | HASH_TYPES = { 40 | 900: "MD4", 41 | 0: ("MD5", 1), 42 | 100: "SHA1", 43 | 1300: "SHA2-224", 44 | 1400: "SHA2-256", 45 | 10800: "SHA2-384", 46 | 1700: "SHA2-512", 47 | 17300: "SHA3-224", 48 | 17400: "SHA3-256", 49 | 17500: "SHA3-384", 50 | 17600: "SHA3-512", 51 | 6000: "RIPEMD-160", 52 | 600: "BLAKE2b-512", 53 | 11700: "GOST R 34.11-2012 (Streebog) 256-bit, big-endian", 54 | 11800: "GOST R 34.11-2012 (Streebog) 512-bit, big-endian", 55 | 6900: "GOST R 34.11-94", 56 | 5100: "Half MD5", 57 | 18700: "Java Object hashCode()", 58 | 17700: "Keccak-224", 59 | 17800: "Keccak-256", 60 | 17900: "Keccak-384", 61 | 18000: "Keccak-512", 62 | 6100: "Whirlpool", 63 | 10100: "SipHash", 64 | 10: "md5($pass.$salt)", 65 | 20: "md5($salt.$pass)", 66 | 3800: "md5($salt.$pass.$salt)", 67 | 3710: "md5($salt.md5($pass))", 68 | 4110: "md5($salt.md5($pass.$salt))", 69 | 4010: "md5($salt.md5($salt.$pass))", 70 | 40: "md5($salt.utf16le($pass))", 71 | 2600: "md5(md5($pass))", 72 | 3910: "md5(md5($pass).md5($salt))", 73 | 4400: "md5(sha1($pass))", 74 | 4300: "md5(strtoupper(md5($pass)))", 75 | 30: "md5(utf16le($pass).$salt)", 76 | 110: "sha1($pass.$salt)", 77 | 120: "sha1($salt.$pass)", 78 | 4900: "sha1($salt.$pass.$salt)", 79 | 4520: "sha1($salt.sha1($pass))", 80 | 140: "sha1($salt.utf16le($pass))", 81 | 19300: "sha1($salt1.$pass.$salt2)", 82 | 14400: "sha1(CX)", 83 | 4700: "sha1(md5($pass))", 84 | 18500: "sha1(md5(md5($pass)))", 85 | 4500: "sha1(sha1($pass))", 86 | 130: "sha1(utf16le($pass).$salt)", 87 | 1410: "sha256($pass.$salt)", 88 | 1420: "sha256($salt.$pass)", 89 | 1440: "sha256($salt.utf16le($pass))", 90 | 1430: "sha256(utf16le($pass).$salt)", 91 | 1710: "sha512($pass.$salt)", 92 | 1720: "sha512($salt.$pass)", 93 | 1740: "sha512($salt.utf16le($pass))", 94 | 1730: "sha512(utf16le($pass).$salt)", 95 | 19500: "Ruby on Rails Restful-Authentication", 96 | 50: "HMAC-MD5 (key = $pass)", 97 | 60: "HMAC-MD5 (key = $salt)", 98 | 150: "HMAC-SHA1 (key = $pass)", 99 | 160: "HMAC-SHA1 (key = $salt)", 100 | 1450: "HMAC-SHA256 (key = $pass)", 101 | 1460: "HMAC-SHA256 (key = $salt)", 102 | 1750: "HMAC-SHA512 (key = $pass)", 103 | 1760: "HMAC-SHA512 (key = $salt)", 104 | 11750: "HMAC-Streebog-256 (key = $pass), big-endian", 105 | 11760: "HMAC-Streebog-256 (key = $salt), big-endian", 106 | 11850: "HMAC-Streebog-512 (key = $pass), big-endian", 107 | 11860: "HMAC-Streebog-512 (key = $salt), big-endian", 108 | 11500: "CRC32", 109 | 14100: "3DES (PT = $salt, key = $pass)", 110 | 14000: "DES (PT = $salt, key = $pass)", 111 | 15400: "ChaCha20", 112 | 14900: "Skip32 (PT = $salt, key = $pass)", 113 | 11900: "PBKDF2-HMAC-MD5", 114 | 12000: "PBKDF2-HMAC-SHA1", 115 | 10900: "PBKDF2-HMAC-SHA256", 116 | 12100: "PBKDF2-HMAC-SHA512", 117 | 8900: "scrypt", 118 | 400: "phpass", 119 | 16900: "Ansible Vault", 120 | 12001: "Atlassian (PBKDF2-HMAC-SHA1)", 121 | 16100: "TACACS+", 122 | 11400: "SIP digest authentication (MD5)", 123 | 5300: "IKE-PSK MD5", 124 | 5400: "IKE-PSK SHA1", 125 | 2500: "WPA-EAPOL-PBKDF2", 126 | 2501: "WPA-EAPOL-PMK", 127 | 16800: "WPA-PMKID-PBKDF2", 128 | 16801: "WPA-PMKID-PMK", 129 | 7300: "IPMI2 RAKP HMAC-SHA1", 130 | 10200: "CRAM-MD5", 131 | 4800: "iSCSI CHAP authentication, MD5(CHAP)", 132 | 16500: "JWT (JSON Web Token)", 133 | 7500: "Kerberos 5 AS-REQ Pre-Auth etype 23", 134 | 18200: "Kerberos 5 AS-REP etype 23", 135 | 13100: "Kerberos 5 TGS-REP etype 23 (RC4-HMAC-MD5)", 136 | 19600: "Kerberos 5 TGS-REP etype 17 (AES128-CTS-HMAC-SHA1-96)", 137 | 19700: "Kerberos 5 TGS-REP etype 18 (AES256-CTS-HMAC-SHA1-96)", 138 | 5500: "NetNTLMv1 / NetNTLMv1+ESS", 139 | 5600: "NetNTLMv2", 140 | 23: "Skype", 141 | 11100: "PostgreSQL CRAM (MD5)", 142 | 11200: "MySQL CRAM (SHA1)", 143 | 8500: "RACF", 144 | 6300: "AIX {smd5}", 145 | 6700: "AIX {ssha1}", 146 | 6400: "AIX {ssha256}", 147 | 6500: "AIX {ssha512}", 148 | 3000: "LM", 149 | 19000: "QNX /etc/shadow (MD5)", 150 | 19100: "QNX /etc/shadow (SHA256)", 151 | 19200: "QNX /etc/shadow (SHA512)", 152 | 15300: "DPAPI masterkey file v1", 153 | 15900: "DPAPI masterkey file v2", 154 | 7200: "GRUB 2", 155 | 12800: "MS-AzureSync PBKDF2-HMAC-SHA256", 156 | 12400: "BSDi Crypt, Extended DES", 157 | 1000: "NTLM", 158 | 122: "macOS v10.4, macOS v10.5, MacOS v10.6", 159 | 1722: "macOS v10.7", 160 | 7100: "macOS v10.8+ (PBKDF2-SHA512)", 161 | 9900: "Radmin2", 162 | 5800: "Samsung Android Password/PIN", 163 | 3200: "bcrypt $2*$, Blowfish (Unix)", 164 | 500: "md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)", 165 | 1500: "descrypt, DES (Unix), Traditional DES", 166 | 7400: "sha256crypt $5$, SHA256 (Unix)", 167 | 1800: "sha512crypt $6$, SHA512 (Unix)", 168 | 13800: "Windows Phone 8+ PIN/password", 169 | 2410: "Cisco-ASA MD5", 170 | 9200: "Cisco-IOS $8$ (PBKDF2-SHA256)", 171 | 9300: "Cisco-IOS $9$ (scrypt)", 172 | 5700: "Cisco-IOS type 4 (SHA256)", 173 | 2400: "Cisco-PIX MD5", 174 | 8100: "Citrix NetScaler", 175 | 1100: "Domain Cached Credentials (DCC), MS Cache", 176 | 2100: "Domain Cached Credentials 2 (DCC2), MS Cache 2", 177 | 7000: "FortiGate (FortiOS)", 178 | 125: "ArubaOS", 179 | 501: "Juniper IVE", 180 | 22: "Juniper NetScreen/SSG (ScreenOS)", 181 | 15100: "Juniper/NetBSD sha1crypt", 182 | 131: "MSSQL (2000)", 183 | 132: "MSSQL (2005)", 184 | 1731: "MSSQL (2012, 2014)", 185 | 12: "PostgreSQL", 186 | 3100: "Oracle H: Type (Oracle 7+)", 187 | 112: "Oracle S: Type (Oracle 11+)", 188 | 12300: "Oracle T: Type (Oracle 12+)", 189 | 200: "MySQL323", 190 | 300: "MySQL4.1/MySQL5", 191 | 8000: "Sybase ASE", 192 | 1421: "hMailServer", 193 | 8300: "DNSSEC (NSEC3)", 194 | 16400: "CRAM-MD5 Dovecot", 195 | 1411: "SSHA-256(Base64), LDAP {SSHA256}", 196 | 1711: "SSHA-512(Base64), LDAP {SSHA512}", 197 | 15000: "FileZilla Server >= 0.9.55", 198 | 12600: "ColdFusion 10+", 199 | 1600: "Apache $apr1$ MD5, md5apr1, MD5 (APR)", 200 | 141: "Episerver 6.x < .NET 4", 201 | 1441: "Episerver 6.x >= .NET 4", 202 | 101: "nsldap, SHA-1(Base64), Netscape LDAP SHA", 203 | 111: "nsldaps, SSHA-1(Base64), Netscape LDAP SSHA", 204 | 7700: "SAP CODVN B (BCODE)", 205 | 7701: "SAP CODVN B (BCODE) from RFC_READ_TABLE", 206 | 7800: "SAP CODVN F/G (PASSCODE)", 207 | 7801: "SAP CODVN F/G (PASSCODE) from RFC_READ_TABLE", 208 | 10300: "SAP CODVN H (PWDSALTEDHASH) iSSHA-1", 209 | 133: "PeopleSoft", 210 | 13500: "PeopleSoft PS_TOKEN", 211 | 8600: "Lotus Notes/Domino 5", 212 | 8700: "Lotus Notes/Domino 6", 213 | 9100: "Lotus Notes/Domino 8", 214 | 12200: "eCryptfs", 215 | 14600: "LUKS", 216 | 13711: "VeraCrypt RIPEMD160 + XTS 512 bit", 217 | 13712: "VeraCrypt RIPEMD160 + XTS 1024 bit", 218 | 13713: "VeraCrypt RIPEMD160 + XTS 1536 bit", 219 | 13741: "VeraCrypt RIPEMD160 + XTS 512 bit + boot-mode", 220 | 13742: "VeraCrypt RIPEMD160 + XTS 1024 bit + boot-mode", 221 | 13743: "VeraCrypt RIPEMD160 + XTS 1536 bit + boot-mode", 222 | 13751: "VeraCrypt SHA256 + XTS 512 bit", 223 | 13752: "VeraCrypt SHA256 + XTS 1024 bit", 224 | 13753: "VeraCrypt SHA256 + XTS 1536 bit", 225 | 13761: "VeraCrypt SHA256 + XTS 512 bit + boot-mode", 226 | 13762: "VeraCrypt SHA256 + XTS 1024 bit + boot-mode", 227 | 13763: "VeraCrypt SHA256 + XTS 1536 bit + boot-mode", 228 | 13721: "VeraCrypt SHA512 + XTS 512 bit", 229 | 13722: "VeraCrypt SHA512 + XTS 1024 bit", 230 | 13723: "VeraCrypt SHA512 + XTS 1536 bit", 231 | 13771: "VeraCrypt Streebog-512 + XTS 512 bit", 232 | 13772: "VeraCrypt Streebog-512 + XTS 1024 bit", 233 | 13773: "VeraCrypt Streebog-512 + XTS 1536 bit", 234 | 13731: "VeraCrypt Whirlpool + XTS 512 bit", 235 | 13732: "VeraCrypt Whirlpool + XTS 1024 bit", 236 | 13733: "VeraCrypt Whirlpool + XTS 1536 bit", 237 | 16700: "FileVault 2", 238 | 12900: "Android FDE (Samsung DEK)", 239 | 8800: "Android FDE <= 4.3", 240 | 18300: "Apple File System (APFS)", 241 | 6211: "TrueCrypt RIPEMD160 + XTS 512 bit", 242 | 6212: "TrueCrypt RIPEMD160 + XTS 1024 bit", 243 | 6213: "TrueCrypt RIPEMD160 + XTS 1536 bit", 244 | 6241: "TrueCrypt RIPEMD160 + XTS 512 bit + boot-mode", 245 | 6242: "TrueCrypt RIPEMD160 + XTS 1024 bit + boot-mode", 246 | 6243: "TrueCrypt RIPEMD160 + XTS 1536 bit + boot-mode", 247 | 6221: "TrueCrypt SHA512 + XTS 512 bit", 248 | 6222: "TrueCrypt SHA512 + XTS 1024 bit", 249 | 6223: "TrueCrypt SHA512 + XTS 1536 bit", 250 | 6231: "TrueCrypt Whirlpool + XTS 512 bit", 251 | 6232: "TrueCrypt Whirlpool + XTS 1024 bit", 252 | 6233: "TrueCrypt Whirlpool + XTS 1536 bit", 253 | 10400: "PDF 1.1 - 1.3 (Acrobat 2 - 4)", 254 | 10410: "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #1", 255 | 10420: "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #2", 256 | 10500: "PDF 1.4 - 1.6 (Acrobat 5 - 8)", 257 | 10600: "PDF 1.7 Level 3 (Acrobat 9)", 258 | 10700: "PDF 1.7 Level 8 (Acrobat 10 - 11)", 259 | 9400: "MS Office 2007", 260 | 9500: "MS Office 2010", 261 | 9600: "MS Office 2013", 262 | 9700: "MS Office <= 2003 $0/$1, MD5 + RC4", 263 | 9710: "MS Office <= 2003 $0/$1, MD5 + RC4, collider #1", 264 | 9720: "MS Office <= 2003 $0/$1, MD5 + RC4, collider #2", 265 | 9800: "MS Office <= 2003 $3/$4, SHA1 + RC4", 266 | 9810: "MS Office <= 2003 $3, SHA1 + RC4, collider #1", 267 | 9820: "MS Office <= 2003 $3, SHA1 + RC4, collider #2", 268 | 18400: "Open Document Format (ODF) 1.2 (SHA-256, AES)", 269 | 18600: "Open Document Format (ODF) 1.1 (SHA-1, Blowfish)", 270 | 16200: "Apple Secure Notes", 271 | 15500: "JKS Java Key Store Private Keys (SHA1)", 272 | 6600: "1Password, agilekeychain", 273 | 8200: "1Password, cloudkeychain", 274 | 9000: "Password Safe v2", 275 | 5200: "Password Safe v3", 276 | 6800: "LastPass + LastPass sniffed", 277 | 13400: "KeePass 1 (AES/Twofish) and KeePass 2 (AES)", 278 | 11300: "Bitcoin/Litecoin wallet.dat", 279 | 16600: "Electrum Wallet (Salt-Type 1-2)", 280 | 12700: "Blockchain, My Wallet", 281 | 15200: "Blockchain, My Wallet, V2", 282 | 18800: "Blockchain, My Wallet, Second Password (SHA256)", 283 | 16300: "Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256", 284 | 15600: "Ethereum Wallet, PBKDF2-HMAC-SHA256", 285 | 15700: "Ethereum Wallet, SCRYPT", 286 | 11600: "7-Zip", 287 | 12500: "RAR3-hp", 288 | 13000: "RAR5", 289 | 14700: "iTunes backup < 10.0", 290 | 14800: "iTunes backup >= 10.0", 291 | 13600: "WinZip", 292 | 18900: "Android Backup", 293 | 13200: "AxCrypt", 294 | 13300: "AxCrypt in-memory SHA1", 295 | 8400: "WBB3 (Woltlab Burning Board)", 296 | 2611: "vBulletin < v3.8.5", 297 | 2711: "vBulletin >= v3.8.5", 298 | 2612: "PHPS", 299 | 121: "SMF (Simple Machines Forum) > v1.1", 300 | 3711: "MediaWiki B type", 301 | 4521: "Redmine", 302 | 10000: "Django (PBKDF2-SHA256)", 303 | 124: "Django (SHA-1)", 304 | 11: "Joomla < 2.5.18", 305 | 13900: "OpenCart", 306 | 11000: "PrestaShop", 307 | 16000: "Tripcode", 308 | 7900: "Drupal7", 309 | 21: "osCommerce, xt:Commerce", 310 | 4522: "PunBB", 311 | 2811: "MyBB 1.2+, IPB2+ (Invision Power Board)", 312 | 18100: "TOTP (HMAC-SHA1)", 313 | 2000: "STDOUT", 314 | 99999: "Plaintext", 315 | } 316 | 317 | FAVORITE_HASHTYPES = { 318 | "AD": [1000, 1100, 3000, 5600, 13100, 19700], 319 | "Web": [0, 100, 900, 1700, 3200, 8900, 12000], 320 | } 321 | 322 | POPULAR_WORDLISTS = [ 323 | "Bitweasel", 324 | "crackstation-human-only.txt", 325 | "wikipedia_de-20160629.txt", 326 | "wikipedia_en-20160629.txt", 327 | "duden_german.txt", 328 | "linkedin.dic", 329 | "10-million-passwords.txt", 330 | "Openwall", 331 | "public_leaks/Rockyou_list_original.txt", 332 | ] 333 | 334 | POPULAR_RULES = [ 335 | "best64.rule", 336 | "dive.rule", 337 | "OneRule.rule", 338 | ] 339 | -------------------------------------------------------------------------------- /hashcathelper/hashcat.py: -------------------------------------------------------------------------------- 1 | """Interface with hashcat""" 2 | 3 | import logging 4 | import pkgutil 5 | import subprocess 6 | import sys 7 | import tempfile 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | NT_RULESET = pkgutil.get_data(__name__, "toggles-lm-ntlm.rule") 12 | 13 | 14 | def prepend_usernames(wordlists, hashfile, directory="."): 15 | """Extract usernames from hashfile, write to a temporary file, and 16 | prepend it to list of wordlists 17 | """ 18 | from hashcathelper.utils import User 19 | 20 | user_file = tempfile.NamedTemporaryFile( 21 | delete=False, 22 | dir=directory, 23 | mode="w", 24 | prefix="userlist_", 25 | ) 26 | with open(hashfile, "r") as fp: 27 | for line in fp.readlines(): 28 | if line.strip(): 29 | u = User(line) 30 | user_file.write(u.username + "\n") 31 | user_file.close() 32 | wordlists.insert(0, user_file.name) 33 | 34 | 35 | def hashcat( 36 | hashcat_bin, 37 | hashfile, 38 | hashtype, 39 | wordlists=[], 40 | ruleset=None, 41 | pwonly=True, 42 | directory=".", 43 | ): 44 | """ 45 | Run hashcat as a subprocess 46 | 47 | Returns: name of a file containing the stdout of hashcat with ``--show`` 48 | """ 49 | 50 | base_command = [ 51 | hashcat_bin, 52 | hashfile, 53 | "--username", 54 | "-m", 55 | str(hashtype), 56 | ] 57 | command = base_command + ["--outfile-autohex-disable"] 58 | if wordlists: 59 | prepend_usernames(wordlists, hashfile, directory=directory) 60 | command = command + ["-a", "0"] + wordlists 61 | # Attack mode wordlist 62 | if ruleset: 63 | command = command + ["-r", ruleset] 64 | else: 65 | # Attack mode brute force, all combinations of 7 character passwords 66 | # (This assumes cracking LM hashes) 67 | command = command + [ 68 | "-a", 69 | "3", 70 | "-i", 71 | "?a?a?a?a?a?a?a", 72 | "--increment-min", 73 | "1", 74 | "--increment-max", 75 | "7", 76 | ] 77 | 78 | log.debug("Running this command: %s" % command) 79 | p = subprocess.Popen( 80 | command, 81 | stdout=sys.stdout, 82 | stderr=subprocess.STDOUT, 83 | ) 84 | p.communicate() 85 | # Check return code. 0-3 is fine (because user cancelled) 86 | # https://github.com/hashcat/hashcat/blob/master/docs/status_codes.txt 87 | if p.returncode not in [0, 1, 2, 3]: 88 | raise RuntimeError("Hashcat exited with an error") 89 | 90 | # Retrieve result 91 | output_file = tempfile.NamedTemporaryFile( 92 | delete=False, 93 | dir=directory, 94 | mode="w", 95 | suffix="_show", 96 | ) 97 | output_file.close() 98 | show_command = base_command + ["--show"] 99 | show_command += [ 100 | "--outfile-format", 101 | "2", 102 | "--outfile", 103 | output_file.name, 104 | ] 105 | 106 | p = subprocess.Popen( 107 | show_command, 108 | stdout=subprocess.PIPE, 109 | stderr=subprocess.PIPE, 110 | ) 111 | p.communicate() 112 | if p.returncode: 113 | raise RuntimeError( 114 | "Hashcat exited with non-zero return code when retrieving result" 115 | ) 116 | 117 | if pwonly: 118 | # Remove user names 119 | from hashcathelper.utils import User 120 | 121 | output_file_cleaned = tempfile.NamedTemporaryFile( 122 | delete=False, 123 | dir=directory, 124 | mode="w", 125 | suffix="pwonly", 126 | ) 127 | with open(output_file.name, "r", encoding="utf-8", errors="ignore") as fp: 128 | for line in fp.readlines(): 129 | u = User(line) 130 | output_file_cleaned.write(u.password + "\n") 131 | output_file_cleaned.close() 132 | result = output_file_cleaned.name 133 | else: 134 | result = output_file.name 135 | return result 136 | 137 | 138 | def crack_pwdump( 139 | hashcat_bin, hashfile, directory, wordlist, ruleset, extra_words=[], skip_lm=False 140 | ): 141 | """ 142 | Crack the hashes in a pwdump file. 143 | 144 | Files like this are generated by Impacket's secretsdump or Meterpreter's 145 | pwdump, for example. A line looks like this: 146 | 147 | :::::: 148 | 149 | First, the LM hashes are cracked in incremental mode. Then, the results 150 | are used with an NTLM rule set to crack the corresponding NT hashes. 151 | Last, the results are added to the crackstation wordlist and mangled 152 | with the OneRule rule set. 153 | """ 154 | 155 | if skip_lm: 156 | log.info("Skipping LM hashes") 157 | wordlists = [wordlist] 158 | else: 159 | lm_result = hashcat( 160 | hashcat_bin, 161 | hashfile, 162 | hashtype=3000, 163 | directory=directory, 164 | ) 165 | 166 | # Write ruleset to file in tempdir 167 | nt_ruleset = tempfile.NamedTemporaryFile( 168 | "wb", 169 | dir=directory, 170 | delete=False, 171 | prefix="rules_", 172 | ) 173 | nt_ruleset.write(NT_RULESET) 174 | nt_ruleset.close() 175 | 176 | nt_result = hashcat( 177 | hashcat_bin, 178 | hashfile, 179 | hashtype=1000, 180 | ruleset=nt_ruleset.name, 181 | wordlists=[lm_result], 182 | directory=directory, 183 | ) 184 | wordlists = [nt_result, wordlist] 185 | 186 | final_result = hashcat( 187 | hashcat_bin, 188 | hashfile, 189 | hashtype=1000, 190 | ruleset=ruleset, 191 | wordlists=wordlists, 192 | pwonly=False, 193 | directory=directory, 194 | ) 195 | return final_result 196 | -------------------------------------------------------------------------------- /hashcathelper/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | grey = "\x1b[38m" 4 | yellow = "\x1b[33m" 5 | green = "\x1b[32m" 6 | red = "\x1b[31m" 7 | bold_red = "\x1b[31;1m" 8 | reset = "\x1b[0m" 9 | 10 | # add success level 11 | logging.SUCCESS = 25 # between WARNING and INFO 12 | logging.addLevelName(logging.SUCCESS, "SUCCESS") 13 | 14 | 15 | def color_map(_format): 16 | FORMATS = { 17 | logging.DEBUG: grey + _format + reset, 18 | logging.INFO: _format, 19 | logging.WARNING: yellow + _format + reset, 20 | logging.ERROR: red + _format + reset, 21 | logging.CRITICAL: bold_red + _format + reset, 22 | logging.SUCCESS: green + _format + reset, 23 | } 24 | return FORMATS 25 | 26 | 27 | class CustomFormatter(logging.Formatter): 28 | """Logging Formatter to add colors""" 29 | 30 | fields = [ 31 | # '%(asctime)s', 32 | "%(levelname)s", 33 | "%(message)s", 34 | ] 35 | _format = " - ".join(fields) 36 | 37 | FORMATS = color_map(_format) 38 | 39 | def format(self, record): 40 | log_fmt = self.FORMATS.get(record.levelno) 41 | formatter = logging.Formatter(log_fmt, "%Y-%m-%d %H:%M:%S") 42 | return formatter.format(record) 43 | 44 | 45 | class CustomFormatterDebug(CustomFormatter): 46 | fields = ["%(asctime)s", "%(filename)s:%(lineno)d", "%(levelname)s", "%(message)s"] 47 | _format = " - ".join(fields) 48 | FORMATS = color_map(_format) 49 | 50 | 51 | def init_logging(loglevel=logging.WARNING, logfile=None): 52 | # create logger 53 | logger = logging.getLogger() 54 | logger.setLevel(loglevel) 55 | 56 | # add success level 57 | setattr( 58 | logger, 59 | "success", 60 | lambda message, *args: logger._log(logging.SUCCESS, message, args), 61 | ) 62 | 63 | # create console handler with a higher log level 64 | ch = logging.StreamHandler() 65 | ch.setLevel(loglevel) 66 | 67 | # create formatter and add it to the handlers 68 | if loglevel in ["DEBUG", logging.DEBUG]: 69 | formatter = CustomFormatterDebug() 70 | else: 71 | formatter = CustomFormatter() 72 | ch.setFormatter(formatter) 73 | 74 | # add the handlers to the logger 75 | logger.addHandler(ch) 76 | 77 | if logfile: 78 | # create file handler which logs even debug messages 79 | fh = logging.FileHandler(logfile) 80 | fh.setLevel(logging.DEBUG) 81 | fh.setFormatter(formatter) 82 | logger.addHandler(fh) 83 | -------------------------------------------------------------------------------- /hashcathelper/md4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright © 2019 James Seo (github.com/kangtastic). 5 | # 6 | # This file is released under the WTFPL, version 2 (wtfpl.net). 7 | # 8 | # md4.py: An implementation of the MD4 hash algorithm in pure Python 3. 9 | # 10 | # Description: Zounds! Yet another rendition of pseudocode from RFC1320! 11 | # Bonus points for the algorithm literally being from 1992. 12 | # 13 | # Usage: Why would anybody use this? This is self-rolled crypto, and 14 | # self-rolled *obsolete* crypto at that. DO NOT USE if you need 15 | # something "performant" or "secure". :P 16 | # 17 | # Anyway, from the command line: 18 | # 19 | # $ ./md4.py [messages] 20 | # 21 | # where [messages] are some strings to be hashed. 22 | # 23 | # In Python, use similarly to hashlib (not that it even has MD4): 24 | # 25 | # from .md4 import MD4 26 | # 27 | # digest = MD4("BEES").hexdigest() 28 | # 29 | # print(digest) # "501af1ef4b68495b5b7e37b15b4cda68" 30 | # 31 | # 32 | # Sample console output: 33 | # 34 | # Testing the MD4 class. 35 | # 36 | # Message: b'' 37 | # Expected: 31d6cfe0d16ae931b73c59d7e0c089c0 38 | # Actual: 31d6cfe0d16ae931b73c59d7e0c089c0 39 | # 40 | # Message: b'The quick brown fox jumps over the lazy dog' 41 | # Expected: 1bee69a46ba811185c194762abaeae90 42 | # Actual: 1bee69a46ba811185c194762abaeae90 43 | # 44 | # Message: b'BEES' 45 | # Expected: 501af1ef4b68495b5b7e37b15b4cda68 46 | # Actual: 501af1ef4b68495b5b7e37b15b4cda68 47 | # 48 | # Comment from hashcathelper maintainer (may 2022): Since MD4 as a legacy 49 | # algorithm is phased out from real crypto libs like openssl for good 50 | # reason, we need this code. Modified to satisfy PEP8. 51 | import struct 52 | 53 | 54 | class MD4: 55 | """An implementation of the MD4 hash algorithm.""" 56 | 57 | width = 32 58 | mask = 0xFFFFFFFF 59 | 60 | # Unlike, say, SHA-1, MD4 uses little-endian. Fascinating! 61 | h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476] 62 | 63 | def __init__(self, msg=None): 64 | """:param ByteString msg: The message to be hashed.""" 65 | if msg is None: 66 | msg = b"" 67 | 68 | self.msg = msg 69 | 70 | # Pre-processing: Total length is a multiple of 512 bits. 71 | ml = len(msg) * 8 72 | msg += b"\x80" 73 | msg += b"\x00" * (-(len(msg) + 8) % 64) 74 | msg += struct.pack("> (MD4.width - n) 148 | return lbits | rbits 149 | -------------------------------------------------------------------------------- /hashcathelper/reporting.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | from collections import OrderedDict 4 | 5 | try: 6 | from html import escape as htmlescape 7 | except ImportError: 8 | from cgi import escape as htmlescape 9 | 10 | from hashcathelper.consts import labels 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class ElementEncoder(json.JSONEncoder): 16 | def default(self, obj): 17 | try: 18 | return obj.as_json() 19 | except AttributeError: 20 | return json.JSONEncoder.default(self, obj) 21 | 22 | 23 | class Element(object): 24 | def __init__(self, label, title=None, formats=["json", "html", "text"]): 25 | self._label = label 26 | if title: 27 | self._title = title 28 | else: 29 | self._title = labels.get(label, label) 30 | self._formats = formats 31 | 32 | def export(self, format): 33 | assert format in ["html", "text", "json"] 34 | if format not in self._formats: 35 | return "" 36 | if self.is_empty(): 37 | return "" 38 | f = getattr(self, "_export_%s" % format) 39 | return f() 40 | 41 | def _export_json(self): 42 | return json.dumps(self, cls=ElementEncoder, indent=2) 43 | 44 | def json(self): 45 | return json.loads(json.dumps(self, cls=ElementEncoder)) 46 | 47 | def is_empty(self): 48 | return False 49 | 50 | 51 | class RelativeQuantity(object): 52 | def __init__(self, numerator, denominator=100): 53 | if denominator == 0: 54 | raise Exception("Denominator can't be zero") 55 | self.numerator = numerator 56 | self.denominator = denominator 57 | 58 | def __int__(self): 59 | return self.numerator 60 | 61 | def __str__(self): 62 | percentage = int(self.numerator / self.denominator * 100 * 100) / 100 63 | result = "%s (%d%%)" % (self.numerator, percentage) 64 | return result 65 | 66 | def as_json(self): 67 | return [self.numerator, self.denominator] 68 | 69 | 70 | class Section(Element): 71 | def __init__(self, *args, **kwargs): 72 | super().__init__(*args, **kwargs) 73 | self._level = 1 74 | self._elements = OrderedDict() 75 | 76 | def __add__(self, element): 77 | if isinstance(element, Section): 78 | element._level = self._level + 1 79 | self._elements[element._label] = element 80 | return self 81 | 82 | def _export_html(self): 83 | result = "%(title)s" % dict( 84 | title=self._title, 85 | level=self._level, 86 | ) 87 | for e in self._elements.values(): 88 | result += e.export("html") 89 | return result 90 | 91 | def _export_text(self): 92 | chars = "=-~.\"'" 93 | result = "%s\n%s\n\n" % ( 94 | self._title, 95 | chars[self._level] * len(self._title), 96 | ) 97 | for e in self._elements.values(): 98 | result += e.export("text") 99 | return result 100 | 101 | def as_json(self): 102 | return self._elements 103 | 104 | def is_empty(self): 105 | return len(self._elements) == 0 106 | 107 | 108 | class Report(Section): 109 | CSS = """ 110 | 169 | """ 170 | 171 | HEADER = ( 172 | """ 173 | Hashcat Helper Report%s 174 | """ 175 | % CSS 176 | ) 177 | 178 | FOOTER = "" 179 | 180 | def __init__(self, *args, **kwargs): 181 | super().__init__(*args, **kwargs) 182 | self._level = 0 183 | 184 | def _export_html(self): 185 | result = self.HEADER 186 | for e in self._elements.values(): 187 | result += e.export("html") 188 | result += self.FOOTER 189 | return result 190 | 191 | def _export_text(self): 192 | rule = "=" * (len(self._title) + 1) 193 | result = "%s\n%s\n%s\n\n" % (rule, self._title, rule) 194 | for e in self._elements.values(): 195 | result += e.export("text") 196 | return result 197 | 198 | 199 | class List(list, Element): 200 | def __init__(self, label, data, *args, **kwargs): 201 | list.__init__(self) 202 | Element.__init__(self, label, *args, **kwargs) 203 | self.extend(data) 204 | 205 | def _export_html(self): 206 | result = "%s
    " % htmlescape(self._title) 207 | for i in self: 208 | result += "
  • %s
  • " % i 209 | result += "
" 210 | return result 211 | 212 | def _export_text(self): 213 | out = self._title + "\n" 214 | out += "\n".join([" * %s" % x for x in self]) 215 | out += "\n\n" 216 | return out 217 | 218 | def as_json(self): 219 | return self 220 | 221 | def is_empty(self): 222 | return len(self) == 0 223 | 224 | 225 | class Table(OrderedDict, Element): 226 | headers = ["Description", "Value"] 227 | 228 | def __init__(self, label, data, *args, **kwargs): 229 | OrderedDict.__init__(self) 230 | Element.__init__(self, label, *args, **kwargs) 231 | self.update(data) 232 | 233 | def _html_card(self, label, value): 234 | """Return a card with a value 235 | 236 | value can be int or `RelativeQuantity` 237 | """ 238 | 239 | donut_section = """ 240 | 241 | 242 | 243 | 245 | 246 | 247 | %(center)s 248 | 249 | 250 | 251 | """ 252 | if isinstance(value, RelativeQuantity): 253 | # RelativeQuantity, draw a donut section 254 | percent = int(value.numerator / value.denominator * 100) 255 | inner = donut_section % dict( 256 | percent=percent, 257 | remaining=100 - percent, 258 | center="%s%%" % percent, 259 | ) 260 | quantity = '
%s
' % inner 261 | else: 262 | quantity = ( 263 | '
%s
' 264 | % value 265 | ) 266 | 267 | label = labels.get(label, label) 268 | label = '
%s
' % label 269 | result = '
%s%s
' % (quantity, label) 270 | 271 | return result 272 | 273 | def _export_html(self): 274 | if not self: 275 | return "" 276 | cards = [self._html_card(k, v) for k, v in self.items()] 277 | result = '
%s
' % "\n".join(cards) 278 | return result 279 | 280 | def _export_text(self): 281 | if not self: 282 | return "" 283 | data = OrderedDict( 284 | (str(labels.get(label, label)), value) for label, value in self.items() 285 | ) 286 | max_len = max(len(x) for x in data) 287 | out = self._title + "\n" 288 | rows = [ 289 | " %s%s%s\n" % (label, " " * (max_len + 2 - len(label)), value) 290 | for label, value in data.items() 291 | ] 292 | out += "".join(rows) 293 | return out + "\n" 294 | 295 | def as_json(self): 296 | return self 297 | 298 | def is_empty(self): 299 | return len(self) == 0 300 | 301 | 302 | class LongTable(Table): 303 | headers = [] 304 | 305 | def _export_html(self): 306 | result = "%s
" % htmlescape(self._title) 307 | result += "" 308 | for k, v in self.items(): 309 | v = htmlescape(", ".join(v)) 310 | result += "" % (k, v) 311 | result += "
%s%s
" 312 | return result 313 | 314 | def _export_text(self): 315 | old = {k: v for k, v in self.items()} 316 | new = {k: ", ".join(v) for k, v in self.items()} 317 | self.clear() 318 | self.update(new) 319 | try: 320 | return super()._export_text() 321 | finally: 322 | self.clear() 323 | self.update(old) 324 | 325 | 326 | class Histogram(Element): 327 | html_width = 500 328 | text_width = 50 329 | text_indent = 4 330 | 331 | def __init__(self, data, *args, **kwargs): 332 | assert isinstance(data, dict) 333 | super().__init__(*args, **kwargs) 334 | self._data = data 335 | 336 | def _export_html(self): 337 | if not self._data: 338 | return "" 339 | out = """ 340 | 342 | %(title)s""" % dict(title=htmlescape(self._title)) 343 | bar_template = """ 344 | 345 | 346 | %(text)s 347 | %(number)s 348 | 349 | """ 350 | 351 | maxval = max(self._data.values()) 352 | y = 0 353 | for k, v in self._data.items(): 354 | if k == "": 355 | k = "" 356 | width_px = int(self.html_width * v / maxval) 357 | row = dict( 358 | text=htmlescape(str(k)), 359 | width=width_px, 360 | y=y, 361 | labelpos=-2, 362 | numberpos=width_px + 2, 363 | number=v, 364 | ) 365 | out += bar_template % row 366 | y += 20 367 | 368 | out += "" 369 | out = ( 370 | "
" 371 | + ("
%s
" % htmlescape(self._title)) 372 | + out 373 | + "
" 374 | ) 375 | return out 376 | 377 | def _export_text(self): 378 | """Create a text-based horizontal bar chart using Unicode""" 379 | if not self._data: 380 | return "%s: No data\n\n" % self._title 381 | maxval = max(self._data.values()) 382 | maxwidth = max([len(str(k)) for k in self._data.keys()]) 383 | blocks = [ 384 | "", # 0/8 385 | "\u258f", # 1/8 386 | "\u258e", # 2/8 387 | "\u258d", # 3/8 388 | "\u258c", # 4/8 389 | "\u258b", # 5/8 390 | "\u258a", # 6/8 391 | "\u2589", # 7/8 392 | "\u2588", # 8/8 393 | ] 394 | 395 | result = "" 396 | if self._title: 397 | result += self._title + "\n" 398 | for k, v in self._data.items(): 399 | if k == "": 400 | k = "" 401 | line = " " * self.text_indent 402 | line += " " * (maxwidth - len(str(k))) + str(k) + " " 403 | length = v / maxval * self.text_width 404 | rounded = int(length) 405 | remainder = int(round((length - rounded) * 8)) 406 | line += blocks[-1] * rounded + blocks[remainder] 407 | if isinstance(v, int): 408 | line += " %d" % v 409 | result += line + "\n" 410 | 411 | return result + "\n" 412 | 413 | def as_json(self): 414 | return OrderedDict(self._data) 415 | 416 | def is_empty(self): 417 | return len(self._data) == 0 418 | -------------------------------------------------------------------------------- /hashcathelper/sql.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | from sqlalchemy import create_engine 5 | from sqlalchemy import Column, Integer, String, Float, DateTime 6 | from sqlalchemy.ext.declarative import declarative_base 7 | from sqlalchemy.orm import sessionmaker 8 | 9 | Base = declarative_base() 10 | 11 | _session = None 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | def get_session(db_uri): 17 | assert db_uri 18 | global _session 19 | if not _session: 20 | db_uri_sanitized = re.sub( 21 | "://(?P[^:]*):[^@]*@", r"://\g:***@", db_uri 22 | ) 23 | log.info("Connection to database: %s" % db_uri_sanitized) 24 | engine = create_engine(db_uri) 25 | Base.metadata.create_all(engine) 26 | Session = sessionmaker(bind=engine) 27 | _session = Session() 28 | return _session 29 | 30 | 31 | class Report(Base): 32 | __tablename__ = "reports" 33 | 34 | id = Column(Integer, primary_key=True) 35 | submitter_email = Column(String) 36 | submission_date = Column(DateTime) 37 | cracking_date = Column(DateTime) 38 | 39 | wordlist = Column(String) 40 | rule_set = Column(String) 41 | hashcathelper_version = Column(String) 42 | hashcat_version = Column(String) 43 | 44 | accounts = Column(Integer) 45 | cracked = Column(Integer) 46 | nonunique = Column(Integer) 47 | user_equals_password = Column(Integer) 48 | lm_hash_count = Column(Integer) 49 | empty_password = Column(Integer) 50 | average_password_length = Column(Float) 51 | largest_baseword_cluster = Column(Integer) 52 | 53 | def columns_to_dict(self): 54 | dict_ = {} 55 | for key in self.__mapper__.c.keys(): 56 | dict_[key] = getattr(self, key) 57 | return dict_ 58 | 59 | 60 | def submit(session, short_report): 61 | r = Report(**short_report) 62 | session.add(r) 63 | session.commit() 64 | return r.id 65 | -------------------------------------------------------------------------------- /hashcathelper/subcommands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/hashcathelper/5f06804a56885c8a25b5c71c5e0498309304aa56/hashcathelper/subcommands/__init__.py -------------------------------------------------------------------------------- /hashcathelper/subcommands/analytics.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from hashcathelper.args import subcommand, argument, parse_config 4 | from hashcathelper.bloodhound import CYPHER_QUERIES 5 | 6 | log = logging.getLogger(__name__) 7 | args = [] 8 | 9 | args.append( 10 | argument( 11 | "-H", 12 | "--hashes", 13 | default=None, 14 | help="path to a file containing hashes. Format: " 15 | "'\\::::::'", 16 | ) 17 | ) 18 | 19 | args.append( 20 | argument( 21 | "-A", 22 | "--accounts-plus-passwords", 23 | default=None, 24 | help="path to a file with the results from the `ntlm` subcommand. " 25 | "Format: ':'", 26 | ) 27 | ) 28 | 29 | args.append( 30 | argument( 31 | "-P", 32 | "--passwords-only", 33 | default=None, 34 | help="path to a file with only passwords; one per line", 35 | ) 36 | ) 37 | 38 | args.append( 39 | argument( 40 | "-F", 41 | "--filter-accounts", 42 | default=None, 43 | help=""" 44 | path to a file containing names of accounts which are subject to analysis, 45 | all other accounts will be filtered out (Example: only active accounts, 46 | only kerberoastable accounts, etc.). If empty, all accounts will be 47 | subject to analysis. Format: one per line, without domain or UPN suffix, 48 | case insensitive. 49 | """, 50 | ) 51 | ) 52 | 53 | args.append( 54 | argument( 55 | "--include-disabled", 56 | default=False, 57 | action="store_true", 58 | help="don't exclude disabled accounts", 59 | ) 60 | ) 61 | 62 | args.append( 63 | argument( 64 | "--include-computer-accounts", 65 | default=False, 66 | action="store_true", 67 | help="don't exclude computer accounts", 68 | ) 69 | ) 70 | 71 | args.append( 72 | argument( 73 | "-B", 74 | "--bloodhound-filter", 75 | default=None, 76 | help=""" 77 | cypher query to match accounts which are subject to analysis. Requires 78 | `--bloodhound-url` to be set. If your BloodHound database contains multiple 79 | domains, you should set `--bloodhound-domain` as well. This parameter can 80 | take the following values to execute a pre-defined cypher query (enabled 81 | accounts only): """ 82 | + ", ".join(CYPHER_QUERIES.keys()), 83 | ) 84 | ) 85 | 86 | args.append( 87 | argument( 88 | "-D", 89 | "--bloodhound-domain", 90 | default=None, 91 | help="""specify the domain in BloodHound related actions 92 | (`--bloodhound-filter`)""", 93 | ) 94 | ) 95 | 96 | args.append( 97 | argument( 98 | "-U", 99 | "--bloodhound-url", 100 | default=None, 101 | help=""" 102 | URL to a Neo4j database containing BloodHound data. Format: 103 | bolt[s]://:@[:]""", 104 | ) 105 | ) 106 | 107 | 108 | args.append( 109 | argument( 110 | "-f", 111 | "--format", 112 | choices=["text", "json", "html", "xlsx"], 113 | default="text", 114 | help="output format (default: %(default)s)", 115 | ) 116 | ) 117 | 118 | args.append( 119 | argument( 120 | "-o", 121 | "--outfile", 122 | default=None, 123 | help="path to an output file (default: stdout)", 124 | ) 125 | ) 126 | 127 | args.append( 128 | argument( 129 | "-m", 130 | "--pw-min-length", 131 | default=6, 132 | type=int, 133 | help="set minimum password length to identify accounts " 134 | "with 'short' passwords (default: %(default)s)", 135 | ) 136 | ) 137 | 138 | 139 | args.append( 140 | argument( 141 | "-d", 142 | "--degree-of-detail", 143 | default=2, 144 | type=int, 145 | help="change the degree of detail of the report (default: %(default)s)", 146 | ) 147 | ) 148 | 149 | 150 | @subcommand(args) 151 | def analytics(args): 152 | """Output interesting statistics""" 153 | from hashcathelper.analytics import create_report, load_lines 154 | from hashcathelper.bloodhound import get_driver, query_neo4j 155 | 156 | if args.filter_accounts: 157 | filter_accounts = load_lines(args.filter_accounts) 158 | else: 159 | filter_accounts = [] 160 | 161 | if args.bloodhound_filter: 162 | driver = get_driver(args.bloodhound_url) 163 | bh_users = query_neo4j( 164 | driver, 165 | args.bloodhound_filter, 166 | domain=args.bloodhound_domain, 167 | ) 168 | else: 169 | bh_users = [] 170 | 171 | config = parse_config(args.config) 172 | report = create_report( 173 | args.hashes, 174 | args.accounts_plus_passwords, 175 | args.passwords_only, 176 | filter_accounts + bh_users, 177 | degree_of_detail=args.degree_of_detail, 178 | pw_min_length=args.pw_min_length, 179 | include_disabled=args.include_disabled, 180 | include_computer_accounts=args.include_computer_accounts, 181 | hibp_db=config.hibp_db, 182 | ) 183 | 184 | if not report: 185 | exit(1) 186 | 187 | if args.format == "xlsx": 188 | # xlsx is a bit special because it only contains the details 189 | xlsx_sanity_check(args) 190 | save_to_xlsx(report, args.outfile) 191 | else: 192 | out = report.export(args.format) 193 | 194 | if args.outfile: 195 | with open(args.outfile, "w", errors="replace") as f: 196 | f.write(out) 197 | else: 198 | print(out, end="") 199 | 200 | 201 | def xlsx_sanity_check(args): 202 | # Do some sanity checks here 203 | 204 | # openpyxl requires py3.6 205 | import sys 206 | 207 | if sys.version_info < (3, 6, 0): 208 | log.critical("XLSX format requires Python 3.6 or higher") 209 | exit(1) 210 | 211 | # Stdout does not make sense for xlsx as it's binary 212 | if not args.outfile: 213 | log.critical("XLSX format requires OUTFILE to be specified.") 214 | exit(1) 215 | 216 | # The xlsx will only contain the details, so not having the details 217 | # makes no sense 218 | if not args.degree_of_detail > 2: 219 | log.critical("XLSX format requires degree of detail greater than 2.") 220 | exit(1) 221 | 222 | 223 | def save_to_xlsx(report, path): 224 | """Saves 'details' from the report to a spreadsheet""" 225 | from collections import OrderedDict 226 | import openpyxl as pyxl 227 | from hashcathelper.consts import labels 228 | 229 | workbook = pyxl.Workbook() 230 | data = report._elements["details"]._elements 231 | 232 | offset = 3 233 | 234 | for k, v in data.items(): 235 | ws = workbook.create_sheet(k) 236 | cell = ws.cell(1, 1, labels.get(k, k)) 237 | cell.font = pyxl.styles.Font(bold=True, size=14) 238 | 239 | if isinstance(v, (list, tuple)): 240 | for i, each in enumerate(v): 241 | ws.cell(offset + i + 1, 1, str(each)) 242 | elif isinstance(v, (dict, OrderedDict)): 243 | for i, key in enumerate(v.keys()): 244 | cell = ws.cell(offset + 1, i + 1, key) 245 | cell.font = pyxl.styles.Font(bold=True) 246 | for i, row in enumerate(v.values()): 247 | if isinstance(row, (list, tuple)): 248 | for j, c in enumerate(row): 249 | ws.cell(offset + j + 2, i + 1, c) 250 | else: 251 | ws.cell(offset + i + 1, 1, c) 252 | 253 | workbook.remove(workbook["Sheet"]) 254 | workbook.save(path) 255 | -------------------------------------------------------------------------------- /hashcathelper/subcommands/bloodhound.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from hashcathelper.args import subcommand, argument 4 | 5 | log = logging.getLogger(__name__) 6 | args = [] 7 | 8 | 9 | def domain_filepath_pair(arg): 10 | if ":" not in arg: 11 | log.critical( 12 | "Argument contains no colon: %s. (Note the new argument format.)" % arg 13 | ) 14 | exit(1) 15 | 16 | domain = arg.split(":")[0] 17 | filepath = arg[len(domain) + 1 :] 18 | 19 | if filepath.startswith("//") and "@" in filepath: 20 | log.critical("Unusual file path: %s. (Note the new argument format.)" % arg) 21 | exit(1) 22 | 23 | return domain, open(filepath, "r") 24 | 25 | 26 | args.append( 27 | argument( 28 | "-t", 29 | "--type", 30 | choices=["same_password", "cracked"], 31 | default="same_password", 32 | help="type of data to add (`SamePassword` relationships or `cracked` boolean attribute; default: %(default)s)", 33 | ) 34 | ) 35 | 36 | 37 | args.append( 38 | argument( 39 | dest="bloodhound_url", 40 | help=""" 41 | URL to a Neo4j database containing BloodHound data. Format: 42 | bolt[s]://:@[:]""", 43 | ) 44 | ) 45 | 46 | 47 | args.append( 48 | argument( 49 | dest="domain_infile", 50 | nargs="+", 51 | type=domain_filepath_pair, 52 | help="domain and path to a report file in JSON format. Format: :", 53 | ) 54 | ) 55 | 56 | 57 | @subcommand(args) 58 | def bloodhound(args): 59 | """Add 'SamePassword' edges to a BloodHound database""" 60 | 61 | if args.type == "same_password": 62 | add_samepassword_relationships(args) 63 | elif args.type == "cracked": 64 | add_cracked_attribute(args) 65 | 66 | 67 | def add_cracked_attribute(args): 68 | import json 69 | 70 | from hashcathelper.bloodhound import get_driver, mark_cracked 71 | 72 | users = [] 73 | for domain, infile in args.domain_infile: 74 | log.info("Reading file: %s" % infile.name) 75 | data = json.load(infile) 76 | print(data.keys()) 77 | if "full_creds" not in data: 78 | log.critical( 79 | "No information about cracked users found in report file (did you use `--degree-of-detail 4`?)" 80 | ) 81 | exit(1) 82 | 83 | users.extend( 84 | ("%s@%s" % (user, domain)).upper() for user in data["full_creds"].keys() 85 | ) 86 | 87 | driver = get_driver(args.bloodhound_url) 88 | mark_cracked(driver, users) 89 | 90 | 91 | def add_samepassword_relationships(args): 92 | import json 93 | import collections 94 | 95 | from hashcathelper.bloodhound import get_driver, add_edges 96 | 97 | clusters = collections.defaultdict(lambda: []) 98 | for domain, infile in args.domain_infile: 99 | log.info("Reading file: %s" % infile.name) 100 | data = json.load(infile) 101 | if "details" not in data or "clusters" not in data["details"]: 102 | log.critical( 103 | "No information about clusters found in report file (did you use `--degree-of-detail 3`?)" 104 | ) 105 | exit(1) 106 | 107 | for password, usernames in data["details"]["clusters"].items(): 108 | clusters[password].extend([u + "@" + domain for u in usernames]) 109 | 110 | driver = get_driver(args.bloodhound_url) 111 | add_edges(driver, clusters.values()) 112 | -------------------------------------------------------------------------------- /hashcathelper/subcommands/db.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | from hashcathelper.args import subcommand, argument, subparsers_map, parse_config 5 | 6 | log = logging.getLogger(__name__) 7 | args = [] 8 | 9 | args.append( 10 | argument( 11 | "--db-uri", 12 | default=None, 13 | help="database URI (default: use value from config)", 14 | ) 15 | ) 16 | 17 | 18 | @subcommand(args) 19 | def db(args): 20 | """Interact with the database""" 21 | 22 | 23 | subparsers = subparsers_map["db"].add_subparsers(help="choose an action") 24 | 25 | 26 | args_submit = [] 27 | 28 | args_submit.append( 29 | argument( 30 | dest="infile", 31 | type=argparse.FileType("r"), 32 | help="path to an input file", 33 | ) 34 | ) 35 | 36 | 37 | @subcommand(args_submit, parent=subparsers) 38 | def submit(args): 39 | """Submit a result to the database""" 40 | import json 41 | 42 | from hashcathelper.sql import submit 43 | from hashcathelper.analytics import create_short_report 44 | 45 | session = get_session(args) 46 | try: 47 | data = json.load(args.infile) 48 | except json.decoder.JSONDecodeError: 49 | log.critical("Could not parse JSON data") 50 | exit(1) 51 | 52 | config = parse_config(args.config) 53 | try: 54 | questions = ask_questions(config) 55 | except KeyboardInterrupt: 56 | log.info("CTRL-C caught, aborting...") 57 | return 58 | 59 | short_report = create_short_report( 60 | questions["submitter_email"], 61 | questions["wordlist"], 62 | questions["rule_set"], 63 | questions["hashcat_version"], 64 | data, 65 | ) 66 | id_ = submit(session, short_report) 67 | 68 | log.info("Entry with ID %d submitted. Thanks for contributing!" % id_) 69 | 70 | 71 | def ask_questions(config): 72 | import subprocess 73 | import os 74 | 75 | print("=" * 79) 76 | print( 77 | """You are about to submit a report from hashcathelper to the database. 78 | Please make sure the data is of high quality and that inactive accounts have 79 | been filtered.""" 80 | ) 81 | print("=" * 79) 82 | print("\n") 83 | print("Press CTRL-C to abort") 84 | print("\n") 85 | 86 | result = {} 87 | result["submitter_email"] = ask_question( 88 | "Please provide your e-mail address in case questions come up" " (optional)", 89 | ) 90 | 91 | result["wordlist"] = ask_question( 92 | "The wordlist you used", 93 | default=os.path.basename(config.wordlist), 94 | ) 95 | 96 | result["rule_set"] = ask_question( 97 | "The rule set you used", 98 | default=os.path.basename(config.rule), 99 | ) 100 | 101 | try: 102 | hashcat_version = ( 103 | subprocess.check_output([config.hashcat_bin, "-V"]).decode().strip() 104 | ) 105 | except Exception as e: 106 | log.error(str(e)) 107 | hashcat_version = "unknown" 108 | 109 | result["hashcat_version"] = ask_question( 110 | "The version of hashcat you used", 111 | default=hashcat_version, 112 | ) 113 | 114 | return result 115 | 116 | 117 | def ask_question(description, default=None, valid=None): 118 | # Import readline so we can use backspace in `input()` 119 | # It automatically wraps `input()`, nothing else needed 120 | import readline # noqa 121 | 122 | print(description) 123 | 124 | while True: 125 | if default: 126 | prompt = "[%s] > " % default 127 | else: 128 | prompt = "> " 129 | answer = input(prompt) 130 | if not answer: 131 | answer = default 132 | if valid and answer not in valid: 133 | print("Invalid answers. Allowed: %s" % valid) 134 | else: 135 | break 136 | 137 | return answer 138 | 139 | 140 | args_query = [] 141 | 142 | args_query.append( 143 | argument( 144 | dest="id", 145 | nargs="?", 146 | help="show details of the entry with this ID; leave empty to list all" 147 | " entries", 148 | ) 149 | ) 150 | 151 | args_query.append( 152 | argument( 153 | "-r", 154 | "--raw", 155 | default=False, 156 | action="store_true", 157 | help="show raw database rows", 158 | ) 159 | ) 160 | 161 | args_query.append( 162 | argument( 163 | "-o", 164 | "--outfile", 165 | default=None, 166 | help="path to an output file (default: stdout)", 167 | ) 168 | ) 169 | 170 | 171 | @subcommand(args_query, parent=subparsers) 172 | def query(args): 173 | """List all entries""" 174 | from hashcathelper.sql import Report 175 | from tabulate import tabulate 176 | 177 | s = get_session(args) 178 | if args.id: 179 | r = s.query(Report).filter_by(id=args.id).one() 180 | data = r.columns_to_dict() 181 | print(tabulate(list(data.items()))) 182 | else: 183 | data = [] 184 | 185 | if args.raw: 186 | headers = list(Report.__mapper__.c.keys()) 187 | else: 188 | headers = "ID Submission E-Mail Accounts".split() 189 | 190 | for r in s.query(Report).order_by(Report.id.asc()).all(): 191 | if args.raw: 192 | data.append(list(r.columns_to_dict().values())) 193 | else: 194 | data.append( 195 | [ 196 | r.id, 197 | r.submission_date.replace(microsecond=0), 198 | r.submitter_email, 199 | r.accounts, 200 | ] 201 | ) 202 | 203 | print( 204 | tabulate( 205 | data, 206 | headers=headers, 207 | ) 208 | ) 209 | 210 | 211 | args_delete = [] 212 | 213 | args_delete.append( 214 | argument( 215 | dest="id", 216 | type=int, 217 | help="delete the entry with this ID", 218 | ) 219 | ) 220 | 221 | args_delete.append( 222 | argument( 223 | "-f", 224 | "--force", 225 | action="store_true", 226 | help="don't ask for confirmation", 227 | ) 228 | ) 229 | 230 | 231 | @subcommand(args_delete, parent=subparsers) 232 | def delete(args): 233 | """Delete an entry""" 234 | from hashcathelper.sql import Report 235 | 236 | if not args.id: 237 | log.error("You must supply an ID.") 238 | exit(1) 239 | s = get_session(args) 240 | 241 | if not args.force: 242 | ans = ask_question( 243 | "You are about to delete entry %d. Are you sure?" % args.id, 244 | "n", 245 | ["y", "n"], 246 | ) 247 | if ans == "n": 248 | log.info("Aborted.") 249 | return 250 | 251 | row = s.query(Report).filter_by(id=args.id) 252 | row.delete() 253 | s.commit() 254 | log.info("Deleted entry %d" % args.id) 255 | 256 | 257 | args_stats = [] 258 | 259 | args_stats.append( 260 | argument( 261 | "-o", 262 | "--outfile", 263 | type=argparse.FileType(mode="w"), 264 | default="-", 265 | help="path to an output file (default: stdout)", 266 | ) 267 | ) 268 | 269 | args_stats.append( 270 | argument( 271 | "-f", 272 | "--format", 273 | choices=["text", "json", "html"], 274 | default="text", 275 | help="output format (default: %(default)s)", 276 | ) 277 | ) 278 | 279 | args_stats.append( 280 | argument( 281 | dest="id", 282 | default=None, 283 | nargs="?", 284 | help="show stats of the entry with this ID; leave empty for last entry; " 285 | "can also be a path to a file containing a full JSON report", 286 | ) 287 | ) 288 | 289 | 290 | @subcommand(args_stats, parent=subparsers) 291 | def stats(args): 292 | """Show statistics for one database entry""" 293 | import os 294 | from hashcathelper.sql import Report 295 | from hashcathelper.analytics import create_short_report 296 | 297 | s = get_session(args) 298 | if args.id: 299 | if os.path.isfile(args.id): 300 | import json 301 | 302 | with open(args.id, "r") as fp: 303 | data = json.load(fp) 304 | r = create_short_report(None, None, None, None, data) 305 | else: 306 | r = s.query(Report).filter_by(id=args.id).one() 307 | if not r: 308 | log.critical("No report found with this ID: %d" % args.id) 309 | exit(1) 310 | else: 311 | r = s.query(Report).order_by(Report.id.desc()).first() 312 | if not r: 313 | log.critical("No report found") 314 | exit(1) 315 | all_entries = s.query(Report).all() 316 | 317 | total_entries = len(all_entries) 318 | total_accounts = sum(e.accounts for e in all_entries) 319 | 320 | if total_entries == 0: 321 | log.critical("Database is empty") 322 | exit(1) 323 | 324 | result = get_stats(r, all_entries) 325 | 326 | if args.format in ["text", "html"]: 327 | from tabulate import tabulate 328 | from hashcathelper.consts import labels 329 | 330 | data = [[labels.get(k, k) + " (%)"] + v for k, v in result.items()] 331 | 332 | # Remove percentage on average pw length 333 | data[-1][0] = data[-1][0][:-4] 334 | 335 | out = "The database holds information about %d accounts in %d entries.\n" % ( 336 | total_accounts, 337 | total_entries, 338 | ) 339 | 340 | out += tabulate( 341 | data, 342 | headers=[ 343 | "Key", 344 | "Value", 345 | "Mean", 346 | "Std. Dev.", 347 | "Perc.", 348 | ], 349 | tablefmt={"text": "plain", "html": "html"}[args.format], 350 | ) 351 | elif args.format == "json": 352 | import json 353 | 354 | out = json.dumps(result, indent=2) 355 | 356 | out += "\n" 357 | args.outfile.write(out) 358 | 359 | 360 | def get_session(args): 361 | from hashcathelper.sql import get_session 362 | 363 | config = parse_config(args.config) 364 | if not args.db_uri: 365 | args.db_uri = config.db_uri 366 | session = get_session(args.db_uri) 367 | return session 368 | 369 | 370 | def normalize(entry, attr): 371 | return getattr(entry, attr) / entry.accounts 372 | 373 | 374 | def mean(numbers): 375 | return float(sum(numbers)) / max(len(numbers), 1) 376 | 377 | 378 | def stddev(numbers): 379 | mu = mean(numbers) 380 | variance = sum([((x - mu) ** 2) for x in numbers]) / len(numbers) 381 | stddev = variance**0.5 382 | return stddev 383 | 384 | 385 | def percentile(x, numbers, higher_is_better=False): 386 | if higher_is_better: 387 | s = sum(n <= x for n in numbers) 388 | else: 389 | s = sum(n >= x for n in numbers) 390 | result = s / len(numbers) * 100 391 | return int(100 * result) / 100 392 | 393 | 394 | def orm_to_dict(entry, relative_quantities, absolute_quantities): 395 | """Convert an ORM object to a dictionary""" 396 | entry_ = {} 397 | for q in relative_quantities: 398 | entry_[q] = normalize(entry, q) 399 | for q in absolute_quantities: 400 | entry_[q] = getattr(entry, q) 401 | return entry_ 402 | 403 | 404 | def get_stats(entry, all_entries): 405 | from collections import OrderedDict 406 | from hashcathelper.utils import prcnt 407 | 408 | relative_quantities = [ 409 | "cracked", 410 | "nonunique", 411 | "user_equals_password", 412 | "lm_hash_count", 413 | "empty_password", 414 | "largest_baseword_cluster", 415 | ] 416 | absolute_quantities = [ 417 | "average_password_length", 418 | ] 419 | higher_is_better = [ 420 | "average_password_length", 421 | ] 422 | 423 | # If entry is a dict, convert to namedtuple that mimics an ORM. 424 | # Yes, we convert it back to a dict in the next step, but it also does 425 | # normalization. 426 | if isinstance(entry, dict): 427 | from collections import namedtuple 428 | 429 | ShortReport = namedtuple("ShortReport", entry.keys()) 430 | entry = ShortReport(**entry) 431 | # Copy ORMs to dicts and normalize relative quantities 432 | entry = orm_to_dict(entry, relative_quantities, absolute_quantities) 433 | all_entries = [ 434 | orm_to_dict(e, relative_quantities, absolute_quantities) for e in all_entries 435 | ] 436 | 437 | # Compute the stats 438 | result = OrderedDict() 439 | for q in relative_quantities + absolute_quantities: 440 | nums = [e[q] for e in all_entries] 441 | p = int( 442 | percentile( 443 | entry[q], 444 | nums, 445 | higher_is_better=(q in higher_is_better), 446 | ) 447 | ) 448 | if q in relative_quantities: 449 | result[q] = [ 450 | prcnt(entry[q], 1), 451 | prcnt(mean(nums), 1), 452 | prcnt(stddev(nums), 1), 453 | p, 454 | ] 455 | else: 456 | result[q] = [ 457 | int(100 * entry[q]) / 100, 458 | int(100 * mean(nums)) / 100, 459 | int(100 * stddev(nums)) / 100, 460 | p, 461 | ] 462 | 463 | return result 464 | -------------------------------------------------------------------------------- /hashcathelper/subcommands/ntlm.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from hashcathelper.args import subcommand, argument, parse_config 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | args = [] 8 | 9 | args.append( 10 | argument( 11 | dest="hashfile", 12 | nargs="+", 13 | help="path to a file containing hashes in pwdump format", 14 | ) 15 | ) 16 | 17 | args.append( 18 | argument( 19 | "-s", 20 | "--suffix", 21 | default=".out", 22 | type=str, 23 | help="results will be placed in the same directory as the input file " 24 | "with the same name, except a suffix will be appended plus a number " 25 | "if the file already exists (default: %(default)s)", 26 | ) 27 | ) 28 | 29 | args.append( 30 | argument( 31 | "-L", 32 | "--skip-lm", 33 | default=False, 34 | action="store_true", 35 | help="Do not crack LM hashes first", 36 | ) 37 | ) 38 | 39 | args.append( 40 | argument( 41 | "-K", 42 | "--keep-tempdir", 43 | default=False, 44 | action="store_true", 45 | help="Do not delete the tempdir at the end", 46 | ) 47 | ) 48 | 49 | 50 | @subcommand(args) 51 | def ntlm(args): 52 | """Crack NTLM hashes from a SAM hive or NTDS.dit""" 53 | import shutil 54 | import tempfile 55 | 56 | config = parse_config(args.config) 57 | 58 | do_sanity_check(config) 59 | 60 | TEMP_DIR = tempfile.mkdtemp( 61 | prefix=args.hashfile[0] + "_hch_", 62 | dir=".", 63 | ) 64 | log.info("Created temporary directory: %s" % TEMP_DIR) 65 | 66 | if len(args.hashfile) == 1: 67 | password_file = run_hashcat(args.hashfile[0], args.skip_lm, config, TEMP_DIR) 68 | result = copy_result(password_file, args.hashfile[0], args.suffix) 69 | else: 70 | log.info("Compiling files into one...") 71 | compiled_hashfile = compile_files(args.hashfile, TEMP_DIR) 72 | password_file = run_hashcat(compiled_hashfile, args.skip_lm, config, TEMP_DIR) 73 | log.info("Decompiling files...") 74 | result = decompile_file(password_file, args.hashfile, args.suffix) 75 | result = ", ".join(result) 76 | log.info("Success! Output is in: %s" % result) 77 | if not args.keep_tempdir: 78 | log.info("Deleting temporary directory...") 79 | shutil.rmtree(TEMP_DIR) 80 | log.info("Done.") 81 | 82 | 83 | def run_hashcat(input_file, skip_lm, config, temp_dir): 84 | from hashcathelper.hashcat import crack_pwdump 85 | 86 | log.info("Starting hashcat...") 87 | _skip_lm = False 88 | if skip_lm: 89 | _skip_lm = True 90 | if not check_lm_hashes(input_file): 91 | log.info("No LM hashes found") 92 | _skip_lm = True 93 | password_file = crack_pwdump( 94 | config.hashcat_bin, 95 | input_file, 96 | temp_dir, 97 | config.wordlist, 98 | config.rule, 99 | skip_lm=_skip_lm, 100 | ) 101 | return password_file 102 | 103 | 104 | def do_sanity_check(config): 105 | import os 106 | 107 | if not config.hashcat_bin: 108 | log.critical("Config value not provided: %s" % "hashcat_bin") 109 | exit(1) 110 | 111 | for path in [config.wordlist, config.rule, config.hashcat_bin]: 112 | if not os.path.isfile(path): 113 | log.critical("File not found: %s" % path) 114 | exit(1) 115 | 116 | 117 | def compile_files(hashfiles, tempdir="."): 118 | """Compile several files into one""" 119 | import tempfile 120 | 121 | result = tempfile.NamedTemporaryFile( 122 | dir=tempdir, delete=False, suffix="compiled" 123 | ).name 124 | 125 | with open(result, "wb") as f_out: 126 | for hf in hashfiles: 127 | with open(hf, "rb") as f_in: 128 | f_out.write(f_in.read()) 129 | 130 | return result 131 | 132 | 133 | def decompile_file(password_file, hashfiles, suffix): 134 | """Reverse the process based on the original hashfiles 135 | 136 | Returns the resulting filenames 137 | """ 138 | from hashcathelper.utils import get_nthash 139 | 140 | # Create dict with original usernames and hashes and create file 141 | # descriptors 142 | usernames = {} 143 | hashes = {} 144 | filenames = [] 145 | 146 | for hf in hashfiles: 147 | filename = find_filename(hf, suffix) 148 | filenames.append(filename) 149 | fp = open(filename, "wb") 150 | with open(hf, "rb") as f: 151 | hashes[fp] = set() 152 | usernames[fp] = set() 153 | for line in f.read().splitlines(): 154 | username, _, _, nthash = line.split(b":")[:4] 155 | usernames[fp].add(username) 156 | hashes[fp].add(b":".join([username, nthash])) 157 | 158 | # Iterate over cracked passwords and store in correct outfile 159 | with open(password_file, "br") as f: 160 | for line in f.read().splitlines(): 161 | username = line.split(b":")[0] 162 | # Find right file descriptor (usernames can be without UPN 163 | # suffix/domain, so mapping is not 1 to 1 164 | # Try username only first 165 | candidates = [fp for fp, names in usernames.items() if username in names] 166 | if len(candidates) == 1: 167 | candidates[0].write(line + b"\n") 168 | else: 169 | # Didn't get a unique result, so hash the password and try 170 | # now to see which original file it was 171 | pw = b":".join(line.split(b":")[1:]) 172 | nthash = get_nthash(pw).encode() 173 | candidates = [ 174 | fp 175 | for fp, names in hashes.items() 176 | if b":".join([username, nthash]) in names 177 | ] 178 | for fp in candidates: 179 | fp.write(line + b"\n") 180 | if not candidates: 181 | log.error( 182 | "Orphaned user: %s:%s" 183 | % ( 184 | username.decode(errors="replace"), 185 | nthash, 186 | ) 187 | ) 188 | 189 | # Close files 190 | for fp in hashes.keys(): 191 | fp.close() 192 | 193 | return filenames 194 | 195 | 196 | def copy_result(src, dest, suffix): 197 | """Copy result to file with correct suffix while making sure not to 198 | overwrite files 199 | 200 | Returns the new filename 201 | """ 202 | import shutil 203 | 204 | target = find_filename(dest, suffix) 205 | shutil.copy(src, target) 206 | return target 207 | 208 | 209 | def find_filename(filename, suffix): 210 | """Find file with correct suffix that doesn't exist""" 211 | import os 212 | import tempfile 213 | 214 | target = filename + suffix 215 | 216 | base = target 217 | count = 0 218 | while os.path.exists(target): 219 | count += 1 220 | target = "%s.%03d" % (base, count) 221 | if count > 1000: 222 | target = tempfile.NamedTemporaryFile(delete=False).name 223 | log.error( 224 | ("Couldn't find a free filename for %s, " "using temporary file: %s") 225 | % (filename, target) 226 | ) 227 | break 228 | 229 | return target 230 | 231 | 232 | def check_lm_hashes(filename): 233 | """Returns True iff the file contains at least one file that contains a 234 | non-empty LM hash""" 235 | from hashcathelper.consts import LM_EMPTY 236 | 237 | with open(filename, "r", newline="\n") as f: 238 | for line in f.readlines(): 239 | if line.split(":")[2] != LM_EMPTY: 240 | return True 241 | return False 242 | -------------------------------------------------------------------------------- /hashcathelper/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import binascii 4 | 5 | 6 | def prcnt(a, b=None): 7 | """Return percentage rounded to two decimals""" 8 | if b: 9 | return int(a / b * 100 * 100) / 100 10 | else: 11 | return int(a * 100) / 100 12 | 13 | 14 | def get_nthash(password): 15 | """Compute NT hash of password (must be bytes)""" 16 | from hashcathelper import md4 17 | 18 | password = password.decode(errors="ignore").encode("utf-16le") 19 | result = md4.MD4(password) 20 | result = result.hexdigest() 21 | return result 22 | 23 | 24 | USER_REGEX = r"^((?P[A-Za-z0-9_\.-]+)\\)?(?P[^:]+)" 25 | USER_PATTERN = re.compile(USER_REGEX + "$") 26 | USER_PATTERN_SUFFIX = re.compile( 27 | r"^(?P[^@]+)@(?P[A-Za-z0-9_\.-]+)$" 28 | ) 29 | USER_PASS_PATTERN = re.compile(USER_REGEX + r":(?P.*)$") 30 | PWDUMP_PATTERN = re.compile( 31 | USER_REGEX + r":(?P[0-9]*):(?P[a-f0-9]{32}):(?P[a-f0-9]{32})" 32 | r":::(?P.*)$" 33 | ) 34 | HEX_PATTERN = re.compile(r"^\$HEX\[(?P[a-f0-9]+)\]$") 35 | 36 | 37 | class User(object): 38 | attributes = "username upn_suffix id lmhash nthash password comment".split() 39 | 40 | def __init__(self, line): 41 | self.line = line 42 | # Try to pass one pattern after another 43 | for p in [ 44 | PWDUMP_PATTERN, 45 | USER_PASS_PATTERN, 46 | USER_PATTERN_SUFFIX, 47 | USER_PATTERN, 48 | ]: 49 | m = p.search(line) 50 | if m: 51 | break 52 | if not m: 53 | raise ValueError("Could not parse line: %s" % line) 54 | 55 | for a in self.attributes: 56 | try: 57 | val = m.group(a) 58 | if val: 59 | val = val.strip() 60 | setattr(self, a, val) 61 | except IndexError: 62 | # "no such group" 63 | setattr(self, a, None) 64 | 65 | if not self.username: 66 | raise ValueError("Could not parse line: %s" % line) 67 | 68 | # Set full_username; won't really be used though 69 | if self.upn_suffix: 70 | self.full_username = "%s\\%s" % ( 71 | self.upn_suffix, 72 | self.username, 73 | ) 74 | else: 75 | self.full_username = self.username 76 | 77 | # Let's also try to convert HEX passwords. 78 | # Hashcat appears to insert spurious non-printable characters 79 | # sometimes. Passwords must be printable so doing the following will 80 | # probably lead to less errors compared to not doing it. 81 | if self.password: 82 | m = HEX_PATTERN.search(self.password) 83 | if m: 84 | bin_p = binascii.unhexlify(m.group("hexascii")) 85 | self.password = bin_p.decode(errors="ignore") 86 | 87 | def is_disabled(self): 88 | if self.comment and "status=Disabled" in self.comment: 89 | return True 90 | return False 91 | 92 | def is_computer_account(self): 93 | return self.username.endswith("$") 94 | 95 | def __eq__(self, b): 96 | if b is None: 97 | return False 98 | if isinstance(b, User): 99 | b = b.username 100 | if not isinstance(b, str): 101 | raise TypeError("Can't compare User object with type %s" % type(b).__name__) 102 | return self.username.lower() == b.lower() 103 | 104 | def __hash__(self): 105 | return hash(self.username.lower()) 106 | 107 | def __str__(self): 108 | return self.full_username 109 | 110 | def __repr__(self): 111 | return "" % str(self) 112 | 113 | def as_json(self): 114 | # needed so this can be serialized by the reporting module 115 | return str(self) 116 | 117 | 118 | def line_binary_search(filename, matchvalue, key=lambda val: val, start=0): 119 | """ 120 | Binary search a file for matching lines. 121 | 122 | Returns a list of matching lines. 123 | 124 | filename - path to file, passed to 'open' 125 | matchvalue - value to match 126 | key - function to extract comparison value from line 127 | 128 | >>> parser = lambda val: int(val.split('\t')[0].strip()) 129 | >>> line_binary_search('sd-arc', 63889187, parser) 130 | ['63889187\t3592559\n', ...] 131 | 132 | Source: 133 | http://www.grantjenks.com/wiki/random/python_binary_search_file_by_line 134 | """ 135 | 136 | # Must be greater than the maximum length of any line. 137 | 138 | max_line_len = 2**8 139 | 140 | pos = start 141 | end = os.path.getsize(filename) 142 | 143 | with open(filename, "rb") as fptr: 144 | # Limit the number of times we binary search. 145 | for rpt in range(50): 146 | last = pos 147 | pos = start + ((end - start) // 2) 148 | fptr.seek(pos) 149 | 150 | # Move the cursor to a newline boundary. 151 | fptr.readline() 152 | line = fptr.readline() 153 | linevalue = key(line) 154 | 155 | if linevalue == matchvalue or pos == last: 156 | # Seek back until we no longer have a match. 157 | while True: 158 | try: 159 | fptr.seek(-max_line_len, 1) 160 | except OSError: 161 | # We seek'ed beyond the beginning of the file 162 | fptr.seek(0) 163 | break 164 | fptr.readline() 165 | if matchvalue != key(fptr.readline()): 166 | break 167 | 168 | # Seek forward to the first match. 169 | for rpt in range(max_line_len): 170 | line = fptr.readline() 171 | linevalue = key(line) 172 | if matchvalue == linevalue: 173 | # Assume each line is unique 174 | return matchvalue, fptr.tell() 175 | else: 176 | # No match was found. 177 | return None, None 178 | 179 | elif linevalue < matchvalue: 180 | start = fptr.tell() 181 | else: 182 | assert linevalue > matchvalue 183 | end = fptr.tell() 184 | else: 185 | raise RuntimeError("binary search failed") 186 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2", "setuptools-git-versioning<2",] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "hashcathelper" 7 | dynamic = ["version"] 8 | authors = [ 9 | {name = "Adrian Vollmer", email = "adrian.vollmer@syss.de"}, 10 | ] 11 | description = "Convenience tool for hashcat" 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | keywords = ["hashcat", "passwords", "cracking", "bloodhound", "analytics", "pentest"] 15 | license = {text = "MIT License"} 16 | classifiers = [ 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | ] 21 | dependencies = [ 22 | 'pyxdg', 23 | 'sqlalchemy', 24 | 'tabulate', 25 | 'openpyxl', 26 | 'neo4j>=4.2, <5.20', 27 | 'importlib-metadata', 28 | 'tqdm', 29 | ] 30 | 31 | [tool.setuptools] 32 | packages = ["hashcathelper"] 33 | 34 | [project.urls] 35 | "Homepage" = "https://github.com/SySS-Research/hashcathelper" 36 | "Bug Tracker" = "https://github.com/SySS-Research/hashcathelper/issues" 37 | 38 | [project.scripts] 39 | hashcathelper = "hashcathelper.__main__:main" 40 | 41 | 42 | [tool.setuptools-git-versioning] 43 | enabled = true 44 | 45 | [project.optional-dependencies] 46 | postgres = ['psycopg2'] 47 | tests = [ 48 | 'pytest', 49 | 'flake8', 50 | 'beautifulsoup4', 51 | 'lxml', 52 | ] 53 | dev = [ 54 | 'tox', 55 | 'build', 56 | ] 57 | 58 | [tool.tox] 59 | legacy_tox_ini = """ 60 | [tox] 61 | envlist = py,lint 62 | isolated_build = True 63 | 64 | [testenv:py] 65 | extras = tests 66 | commands = pytest -W ignore::DeprecationWarning -v {posargs} tests 67 | deps = .[tests] 68 | 69 | [testenv:lint] 70 | skip_install = true 71 | deps = .[tests] 72 | commands = flake8 hashcathelper tests 73 | """ 74 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 6 | 7 | 8 | CONFIG = """[DEFAULT] 9 | 10 | hashcat_bin = /usr/bin/hashcat 11 | rule = %(testdir)s/OneRule.rule 12 | wordlist = %(testdir)s/words 13 | hash_speed = 60000 14 | db_uri = sqlite:///%(sqlite_path)s 15 | """ 16 | 17 | 18 | @pytest.fixture(scope='session') 19 | def config_file(): 20 | import tempfile 21 | fp = tempfile.NamedTemporaryFile(mode='w', delete=False, 22 | prefix="hch_configfile") 23 | db = tempfile.NamedTemporaryFile(mode='wb', delete=False, 24 | prefix="hch_sqlite_db") 25 | db.close() 26 | fp.write(CONFIG % { 27 | 'testdir': SCRIPT_PATH, 28 | 'sqlite_path': db.name, 29 | }) 30 | fp.close() 31 | yield fp.name 32 | # TODO delete files? 33 | -------------------------------------------------------------------------------- /tests/hash.txt.json: -------------------------------------------------------------------------------- 1 | { 2 | "statistics": { 3 | "key_quantities": { 4 | "total_accounts": 1327, 5 | "removed": 15, 6 | "user_equals_password": [ 7 | 1, 8 | 1312 9 | ], 10 | "accounts": 1312, 11 | "cracked": [ 12 | 1012, 13 | 1312 14 | ], 15 | "cracked_computer_accounts": 0, 16 | "lm_hash_count": [ 17 | 17, 18 | 1312 19 | ], 20 | "nonunique": [ 21 | 994, 22 | 1312 23 | ], 24 | "empty_password": [ 25 | 134, 26 | 1312 27 | ], 28 | "average_password_length": 5.5, 29 | "median_password_length": 6.0, 30 | "average_character_classes": 1.17 31 | }, 32 | "cluster_count": { 33 | "2": 16, 34 | "3": 11, 35 | "4": 5, 36 | "5": 5, 37 | "6": 5, 38 | "7": 4, 39 | "9": 1, 40 | "11": 2, 41 | "12": 1, 42 | "13": 2, 43 | "14": 1, 44 | "15": 1, 45 | "18": 2, 46 | "19": 2, 47 | "23": 1, 48 | "25": 1, 49 | "28": 2, 50 | "30": 1, 51 | "37": 1, 52 | "40": 1, 53 | "53": 1, 54 | "54": 1, 55 | "59": 1, 56 | "69": 1, 57 | "74": 1, 58 | "134": 1 59 | }, 60 | "password_length_count": { 61 | "0": 132, 62 | "3": 7, 63 | "4": 29, 64 | "5": 106, 65 | "6": 411, 66 | "7": 183, 67 | "8": 143, 68 | "9": 1 69 | }, 70 | "char_class_count": { 71 | "0": 132, 72 | "1": 571, 73 | "2": 308, 74 | "3": 1 75 | } 76 | }, 77 | "sensitive_data": { 78 | "top10_passwords": { 79 | "": 132, 80 | "12345": 74, 81 | "abc123": 69, 82 | "123456": 59, 83 | "passwd": 54, 84 | "password": 53, 85 | "newpass": 40, 86 | "notused": 37, 87 | "Hockey": 30, 88 | "Maddock": 28 89 | }, 90 | "top10_basewords": { 91 | "abc": 71, 92 | "passwd": 54, 93 | "password": 53, 94 | "internet": 41, 95 | "newpass": 40, 96 | "notused": 37, 97 | "hockey": 30, 98 | "mickey": 30, 99 | "maddock": 28, 100 | "newuser": 25 101 | } 102 | }, 103 | "details": { 104 | "clusters": { 105 | "Purple": [ 106 | "User00000", 107 | "User00079", 108 | "User00292", 109 | "User00364", 110 | "User00375", 111 | "User00913" 112 | ], 113 | "test": [ 114 | "User00001", 115 | "User00013", 116 | "User00076", 117 | "User00206", 118 | "User00387", 119 | "User00462", 120 | "User00539", 121 | "User00595", 122 | "User00597", 123 | "User00610", 124 | "User00771", 125 | "User00829", 126 | "User00837" 127 | ], 128 | "123456": [ 129 | "User00002", 130 | "User00005", 131 | "User00037", 132 | "User00067", 133 | "User00083", 134 | "User00121", 135 | "User00161", 136 | "User00183", 137 | "User00184", 138 | "User00191", 139 | "User00194", 140 | "User00195", 141 | "User00197", 142 | "User00220", 143 | "User00236", 144 | "User00277", 145 | "User00286", 146 | "User00288", 147 | "User00311", 148 | "User00317", 149 | "User00331", 150 | "User00338", 151 | "User00344", 152 | "User00397", 153 | "User00410", 154 | "User00419", 155 | "User00426", 156 | "User00428", 157 | "User00438", 158 | "User00479", 159 | "User00486", 160 | "User00541", 161 | "User00547", 162 | "User00551", 163 | "User00558", 164 | "User00559", 165 | "User00572", 166 | "User00606", 167 | "User00621", 168 | "User00627", 169 | "User00629", 170 | "User00637", 171 | "User00643", 172 | "User00677", 173 | "User00687", 174 | "User00689", 175 | "User00695", 176 | "User00729", 177 | "User00766", 178 | "User00825", 179 | "User00861", 180 | "User00886", 181 | "User00895", 182 | "User00896", 183 | "User00904", 184 | "User00905", 185 | "User00941", 186 | "User00944", 187 | "User00955" 188 | ], 189 | "abc123": [ 190 | "User00003", 191 | "User00015", 192 | "User00032", 193 | "User00043", 194 | "User00046", 195 | "User00048", 196 | "User00102", 197 | "User00105", 198 | "User00129", 199 | "User00135", 200 | "User00136", 201 | "User00144", 202 | "User00156", 203 | "User00167", 204 | "User00174", 205 | "User00187", 206 | "User00189", 207 | "User00201", 208 | "User00223", 209 | "User00264", 210 | "User00281", 211 | "User00320", 212 | "User00332", 213 | "User00335", 214 | "User00337", 215 | "User00353", 216 | "User00355", 217 | "User00362", 218 | "User00372", 219 | "User00391", 220 | "User00405", 221 | "User00473", 222 | "User00507", 223 | "User00508", 224 | "User00538", 225 | "User00562", 226 | "User00575", 227 | "User00582", 228 | "User00596", 229 | "User00603", 230 | "User00608", 231 | "User00618", 232 | "User00670", 233 | "User00690", 234 | "User00691", 235 | "User00696", 236 | "User00719", 237 | "User00742", 238 | "User00745", 239 | "User00750", 240 | "User00784", 241 | "User00789", 242 | "User00790", 243 | "User00795", 244 | "User00810", 245 | "User00812", 246 | "User00815", 247 | "User00834", 248 | "User00848", 249 | "User00856", 250 | "User00870", 251 | "User00881", 252 | "User00888", 253 | "User00915", 254 | "User00929", 255 | "User00938", 256 | "User00951", 257 | "User00965", 258 | "User00970" 259 | ], 260 | "Hockey": [ 261 | "User00004", 262 | "User00011", 263 | "User00054", 264 | "User00140", 265 | "User00163", 266 | "User00203", 267 | "User00227", 268 | "User00243", 269 | "User00284", 270 | "User00312", 271 | "User00381", 272 | "User00383", 273 | "User00404", 274 | "User00457", 275 | "User00604", 276 | "User00617", 277 | "User00623", 278 | "User00626", 279 | "User00657", 280 | "User00682", 281 | "User00705", 282 | "User00711", 283 | "User00743", 284 | "User00800", 285 | "User00813", 286 | "User00827", 287 | "User00844", 288 | "User00898", 289 | "User00909", 290 | "User00966" 291 | ], 292 | "ou812": [ 293 | "User00006", 294 | "User00321", 295 | "User00363", 296 | "User00439", 297 | "User00460", 298 | "User00477", 299 | "User00630", 300 | "User00792", 301 | "User00830", 302 | "User00962", 303 | "User00990" 304 | ], 305 | "password": [ 306 | "User00007", 307 | "User00012", 308 | "User00020", 309 | "User00053", 310 | "User00069", 311 | "User00085", 312 | "User00164", 313 | "User00173", 314 | "User00177", 315 | "User00209", 316 | "User00222", 317 | "User00232", 318 | "User00238", 319 | "User00247", 320 | "User00276", 321 | "User00279", 322 | "User00298", 323 | "User00349", 324 | "User00409", 325 | "User00415", 326 | "User00480", 327 | "User00525", 328 | "User00537", 329 | "User00540", 330 | "User00561", 331 | "User00563", 332 | "User00567", 333 | "User00587", 334 | "User00598", 335 | "User00609", 336 | "User00615", 337 | "User00622", 338 | "User00632", 339 | "User00646", 340 | "User00649", 341 | "User00672", 342 | "User00706", 343 | "User00720", 344 | "User00736", 345 | "User00748", 346 | "User00779", 347 | "User00782", 348 | "User00785", 349 | "User00796", 350 | "User00817", 351 | "User00854", 352 | "User00872", 353 | "User00897", 354 | "User00936", 355 | "User00942", 356 | "User00945", 357 | "User00963", 358 | "User00989" 359 | ], 360 | "notused": [ 361 | "User00008", 362 | "User00024", 363 | "User00030", 364 | "User00041", 365 | "User00074", 366 | "User00099", 367 | "User00160", 368 | "User00204", 369 | "User00208", 370 | "User00210", 371 | "User00280", 372 | "User00291", 373 | "User00295", 374 | "User00314", 375 | "User00347", 376 | "User00350", 377 | "User00351", 378 | "User00395", 379 | "User00420", 380 | "User00430", 381 | "User00440", 382 | "User00443", 383 | "User00468", 384 | "User00524", 385 | "User00564", 386 | "User00581", 387 | "User00593", 388 | "User00642", 389 | "User00644", 390 | "User00654", 391 | "User00717", 392 | "User00775", 393 | "User00851", 394 | "User00853", 395 | "User00943", 396 | "User00956", 397 | "User00999" 398 | ], 399 | "Maddock": [ 400 | "User00009", 401 | "User00047", 402 | "User00064", 403 | "User00068", 404 | "User00091", 405 | "User00113", 406 | "User00120", 407 | "User00162", 408 | "User00193", 409 | "User00265", 410 | "User00315", 411 | "User00365", 412 | "User00384", 413 | "User00398", 414 | "User00472", 415 | "User00483", 416 | "User00532", 417 | "User00605", 418 | "User00624", 419 | "User00693", 420 | "User00716", 421 | "User00822", 422 | "User00832", 423 | "User00950", 424 | "User00961", 425 | "User00971", 426 | "User00975", 427 | "User00985" 428 | ], 429 | "mickey": [ 430 | "User00010", 431 | "User00390" 432 | ], 433 | "newuser": [ 434 | "User00014", 435 | "User00072", 436 | "User00098", 437 | "User00100", 438 | "User00181", 439 | "User00272", 440 | "User00302", 441 | "User00427", 442 | "User00433", 443 | "User00434", 444 | "User00444", 445 | "User00448", 446 | "User00585", 447 | "User00589", 448 | "User00625", 449 | "User00634", 450 | "User00652", 451 | "User00665", 452 | "User00703", 453 | "User00710", 454 | "User00816", 455 | "User00840", 456 | "User00878", 457 | "User00925", 458 | "User00988" 459 | ], 460 | "0246": [ 461 | "User00016", 462 | "User00114", 463 | "User00254", 464 | "User00290", 465 | "User00611", 466 | "User00684" 467 | ], 468 | "1q2w3e": [ 469 | "User00017", 470 | "User00342" 471 | ], 472 | "Beavis": [ 473 | "User00018", 474 | "User00033", 475 | "User00050", 476 | "User00211", 477 | "User00367", 478 | "User00386", 479 | "User00411", 480 | "User00456", 481 | "User00536", 482 | "User00688", 483 | "User00759", 484 | "User00819", 485 | "User00917", 486 | "User00930" 487 | ], 488 | "Justin": [ 489 | "User00019", 490 | "User00260", 491 | "User00327" 492 | ], 493 | "Cowboys": [ 494 | "User00021", 495 | "User00036", 496 | "User00218", 497 | "User00224", 498 | "User00270", 499 | "User00366", 500 | "User00422", 501 | "User00496", 502 | "User00560", 503 | "User00669", 504 | "User00757", 505 | "User00864", 506 | "User00868", 507 | "User00954", 508 | "User00976" 509 | ], 510 | "money": [ 511 | "User00022", 512 | "User00080", 513 | "User00358", 514 | "User00382", 515 | "User00469", 516 | "User00534", 517 | "User00841", 518 | "User00946", 519 | "User00980" 520 | ], 521 | "Mickey": [ 522 | "User00023", 523 | "User00116", 524 | "User00150", 525 | "User00153", 526 | "User00221", 527 | "User00250", 528 | "User00316", 529 | "User00329", 530 | "User00345", 531 | "User00385", 532 | "User00423", 533 | "User00447", 534 | "User00455", 535 | "User00492", 536 | "User00509", 537 | "User00594", 538 | "User00660", 539 | "User00727", 540 | "User00746", 541 | "User00807", 542 | "User00814", 543 | "User00871", 544 | "User00900", 545 | "User00920", 546 | "User00952", 547 | "User00960", 548 | "User00964", 549 | "User00977" 550 | ], 551 | "": [ 552 | "User00025", 553 | "User00035", 554 | "User00040", 555 | "User00052", 556 | "User00075", 557 | "User00097", 558 | "User00109", 559 | "User00112", 560 | "User00117", 561 | "User00124", 562 | "User00125", 563 | "User00132", 564 | "User00134", 565 | "User00141", 566 | "User00142", 567 | "User00152", 568 | "User00169", 569 | "User00172", 570 | "User00186", 571 | "User00199", 572 | "User00217", 573 | "User00225", 574 | "User00228", 575 | "User00234", 576 | "User00237", 577 | "User00248", 578 | "User00249", 579 | "User00255", 580 | "User00266", 581 | "User00273", 582 | "User00282", 583 | "User00285", 584 | "User00289", 585 | "User00306", 586 | "User00307", 587 | "User00309", 588 | "User00310", 589 | "User00324", 590 | "User00333", 591 | "User00360", 592 | "User00368", 593 | "User00369", 594 | "User00371", 595 | "User00373", 596 | "User00374", 597 | "User00402", 598 | "User00406", 599 | "User00412", 600 | "User00417", 601 | "User00418", 602 | "User00429", 603 | "User00432", 604 | "User00465", 605 | "User00466", 606 | "User00474", 607 | "User00487", 608 | "User00490", 609 | "User00493", 610 | "User00497", 611 | "User00498", 612 | "User00500", 613 | "User00502", 614 | "User00505", 615 | "User00512", 616 | "User00514", 617 | "User00531", 618 | "User00542", 619 | "User00544", 620 | "User00545", 621 | "User00546", 622 | "User00553", 623 | "User00557", 624 | "User00583", 625 | "User00586", 626 | "User00602", 627 | "User00607", 628 | "User00613", 629 | "User00648", 630 | "User00651", 631 | "User00653", 632 | "User00656", 633 | "User00658", 634 | "User00663", 635 | "User00664", 636 | "User00694", 637 | "User00697", 638 | "User00699", 639 | "User00721", 640 | "User00723", 641 | "User00733", 642 | "User00739", 643 | "User00740", 644 | "User00753", 645 | "User00755", 646 | "User00758", 647 | "User00763", 648 | "User00764", 649 | "User00769", 650 | "User00773", 651 | "User00793", 652 | "User00823", 653 | "User00828", 654 | "User00847", 655 | "User00862", 656 | "User00863", 657 | "User00877", 658 | "User00879", 659 | "User00880", 660 | "User00883", 661 | "User00884", 662 | "User00902", 663 | "User00906", 664 | "User00916", 665 | "User00922", 666 | "User00932", 667 | "User00934", 668 | "User00935", 669 | "User00957", 670 | "User00959", 671 | "User00983", 672 | "User00993", 673 | "User00995", 674 | "User01300", 675 | "User01301", 676 | "User01302", 677 | "User01304", 678 | "User01305", 679 | "User01306", 680 | "User01307", 681 | "User01308", 682 | "User01309", 683 | "User01311", 684 | "User01312", 685 | "User01314" 686 | ], 687 | "newpass": [ 688 | "User00026", 689 | "User00051", 690 | "User00065", 691 | "User00092", 692 | "User00133", 693 | "User00158", 694 | "User00200", 695 | "User00239", 696 | "User00297", 697 | "User00313", 698 | "User00322", 699 | "User00328", 700 | "User00461", 701 | "User00463", 702 | "User00482", 703 | "User00491", 704 | "User00554", 705 | "User00571", 706 | "User00574", 707 | "User00576", 708 | "User00579", 709 | "User00674", 710 | "User00679", 711 | "User00680", 712 | "User00685", 713 | "User00709", 714 | "User00731", 715 | "User00737", 716 | "User00756", 717 | "User00761", 718 | "User00768", 719 | "User00770", 720 | "User00780", 721 | "User00786", 722 | "User00846", 723 | "User00867", 724 | "User00889", 725 | "User00901", 726 | "User00926", 727 | "User00953" 728 | ], 729 | "12345678": [ 730 | "User00027", 731 | "User00063", 732 | "User00066", 733 | "User00089", 734 | "User00119", 735 | "User00157", 736 | "User00354", 737 | "User00393", 738 | "User00442", 739 | "User00515", 740 | "User00612", 741 | "User00638", 742 | "User00702", 743 | "User00787", 744 | "User00839", 745 | "User00857", 746 | "User00924", 747 | "User00928", 748 | "User00991" 749 | ], 750 | "a1b2c3": [ 751 | "User00028", 752 | "User00451" 753 | ], 754 | "Jessica": [ 755 | "User00029", 756 | "User00049" 757 | ], 758 | "wheeling": [ 759 | "User00031", 760 | "User00325", 761 | "User00421" 762 | ], 763 | "internet": [ 764 | "User00034", 765 | "User00057", 766 | "User00059", 767 | "User00061", 768 | "User00082", 769 | "User00115", 770 | "User00168", 771 | "User00205", 772 | "User00251", 773 | "User00263", 774 | "User00278", 775 | "User00339", 776 | "User00392", 777 | "User00436", 778 | "User00548", 779 | "User00568", 780 | "User00590", 781 | "User00781", 782 | "User00783", 783 | "User00788", 784 | "User00820", 785 | "User00874", 786 | "User00947" 787 | ], 788 | "tigger": [ 789 | "User00038", 790 | "User00226", 791 | "User00475", 792 | "User00504", 793 | "User00859", 794 | "User00914" 795 | ], 796 | "Internet": [ 797 | "User00039", 798 | "User00073", 799 | "User00093", 800 | "User00198", 801 | "User00258", 802 | "User00352", 803 | "User00379", 804 | "User00401", 805 | "User00471", 806 | "User00640", 807 | "User00662", 808 | "User00724", 809 | "User00798", 810 | "User00809", 811 | "User00849", 812 | "User00850", 813 | "User00869", 814 | "User00882" 815 | ], 816 | "mustang": [ 817 | "User00042", 818 | "User00045", 819 | "User00246", 820 | "User00370", 821 | "User00659", 822 | "User00777", 823 | "User00908" 824 | ], 825 | "passwd": [ 826 | "User00044", 827 | "User00107", 828 | "User00122", 829 | "User00138", 830 | "User00145", 831 | "User00155", 832 | "User00165", 833 | "User00171", 834 | "User00176", 835 | "User00178", 836 | "User00188", 837 | "User00245", 838 | "User00262", 839 | "User00268", 840 | "User00271", 841 | "User00287", 842 | "User00294", 843 | "User00303", 844 | "User00318", 845 | "User00323", 846 | "User00356", 847 | "User00376", 848 | "User00437", 849 | "User00445", 850 | "User00499", 851 | "User00501", 852 | "User00513", 853 | "User00519", 854 | "User00523", 855 | "User00555", 856 | "User00616", 857 | "User00633", 858 | "User00641", 859 | "User00675", 860 | "User00708", 861 | "User00732", 862 | "User00738", 863 | "User00741", 864 | "User00751", 865 | "User00806", 866 | "User00821", 867 | "User00838", 868 | "User00845", 869 | "User00866", 870 | "User00873", 871 | "User00890", 872 | "User00892", 873 | "User00912", 874 | "User00939", 875 | "User00967", 876 | "User00972", 877 | "User00978", 878 | "User00981", 879 | "User00987" 880 | ], 881 | "jordan": [ 882 | "User00055", 883 | "User00467", 884 | "User00715", 885 | "User00754", 886 | "User00778", 887 | "User00933" 888 | ], 889 | "12345": [ 890 | "User00056", 891 | "User00070", 892 | "User00071", 893 | "User00104", 894 | "User00110", 895 | "User00127", 896 | "User00128", 897 | "User00139", 898 | "User00149", 899 | "User00159", 900 | "User00202", 901 | "User00214", 902 | "User00229", 903 | "User00242", 904 | "User00267", 905 | "User00274", 906 | "User00283", 907 | "User00330", 908 | "User00336", 909 | "User00340", 910 | "User00343", 911 | "User00361", 912 | "User00377", 913 | "User00378", 914 | "User00403", 915 | "User00414", 916 | "User00424", 917 | "User00431", 918 | "User00435", 919 | "User00446", 920 | "User00450", 921 | "User00459", 922 | "User00481", 923 | "User00511", 924 | "User00517", 925 | "User00518", 926 | "User00521", 927 | "User00529", 928 | "User00533", 929 | "User00543", 930 | "User00550", 931 | "User00578", 932 | "User00591", 933 | "User00600", 934 | "User00620", 935 | "User00635", 936 | "User00639", 937 | "User00673", 938 | "User00676", 939 | "User00683", 940 | "User00698", 941 | "User00701", 942 | "User00722", 943 | "User00725", 944 | "User00744", 945 | "User00762", 946 | "User00774", 947 | "User00776", 948 | "User00791", 949 | "User00802", 950 | "User00804", 951 | "User00818", 952 | "User00824", 953 | "User00842", 954 | "User00876", 955 | "User00891", 956 | "User00907", 957 | "User00911", 958 | "User00918", 959 | "User00921", 960 | "User00927", 961 | "User00937", 962 | "User00994", 963 | "User00996" 964 | ], 965 | "fiction": [ 966 | "User00058", 967 | "User00084", 968 | "User00182", 969 | "User00259", 970 | "User00592", 971 | "User00681", 972 | "User00704", 973 | "User00718", 974 | "User00752", 975 | "User00808", 976 | "User00858" 977 | ], 978 | "123": [ 979 | "User00060", 980 | "User00086", 981 | "User00108", 982 | "User00213", 983 | "User00359", 984 | "User00836", 985 | "User00984" 986 | ], 987 | "Qwerty": [ 988 | "User00077", 989 | "User00346", 990 | "User00707" 991 | ], 992 | "foobar": [ 993 | "User00078", 994 | "User00081", 995 | "User00484", 996 | "User00730", 997 | "User00903" 998 | ], 999 | "Sports": [ 1000 | "User00087", 1001 | "User00101", 1002 | "User00154", 1003 | "User00855" 1004 | ], 1005 | "jennifer": [ 1006 | "User00088", 1007 | "User00300", 1008 | "User00678" 1009 | ], 1010 | "Dakota": [ 1011 | "User00090", 1012 | "User00556", 1013 | "User00565", 1014 | "User00998" 1015 | ], 1016 | "chris": [ 1017 | "User00095", 1018 | "User00348", 1019 | "User00453", 1020 | "User00979" 1021 | ], 1022 | "orange": [ 1023 | "User00096", 1024 | "User00240", 1025 | "User00269", 1026 | "User00396", 1027 | "User00449", 1028 | "User00458", 1029 | "User00464", 1030 | "User00614", 1031 | "User00631", 1032 | "User00686", 1033 | "User00726", 1034 | "User00860", 1035 | "User00919" 1036 | ], 1037 | "Jordan": [ 1038 | "User00103", 1039 | "User00146", 1040 | "User00170", 1041 | "User00299", 1042 | "User00489", 1043 | "User00549", 1044 | "User00661", 1045 | "User00747", 1046 | "User00794", 1047 | "User00797", 1048 | "User00799", 1049 | "User00805", 1050 | "User00835", 1051 | "User00875", 1052 | "User00899", 1053 | "User00910", 1054 | "User00931", 1055 | "User00940", 1056 | "User00958" 1057 | ], 1058 | "1234": [ 1059 | "User00106", 1060 | "User00130", 1061 | "User00400", 1062 | "User00452", 1063 | "User00566", 1064 | "User00570", 1065 | "User00735" 1066 | ], 1067 | "qwerty": [ 1068 | "User00111", 1069 | "User00196", 1070 | "User00257", 1071 | "User00296", 1072 | "User00326", 1073 | "User00478", 1074 | "User00569", 1075 | "User00584", 1076 | "User00636", 1077 | "User00645", 1078 | "User00668", 1079 | "User00833" 1080 | ], 1081 | "computer": [ 1082 | "User00118", 1083 | "User00137", 1084 | "User00180", 1085 | "User00185", 1086 | "User00190", 1087 | "User00216", 1088 | "User00230", 1089 | "User00244", 1090 | "User00261", 1091 | "User00305", 1092 | "User00413", 1093 | "User00528", 1094 | "User00552", 1095 | "User00580", 1096 | "User00628", 1097 | "User00647", 1098 | "User00700", 1099 | "User00803" 1100 | ], 1101 | "clever": [ 1102 | "User00131", 1103 | "User00253", 1104 | "User00510", 1105 | "User00526", 1106 | "User00577" 1107 | ], 1108 | "bandit": [ 1109 | "User00143", 1110 | "User00319", 1111 | "User00341" 1112 | ], 1113 | "shadow": [ 1114 | "User00147", 1115 | "User00166", 1116 | "User00843" 1117 | ], 1118 | "david": [ 1119 | "User00148", 1120 | "User00301", 1121 | "User00494", 1122 | "User00619", 1123 | "User00767" 1124 | ], 1125 | "Dallas": [ 1126 | "User00151", 1127 | "User00522" 1128 | ], 1129 | "sunshine": [ 1130 | "User00175", 1131 | "User00852", 1132 | "User00986" 1133 | ], 1134 | "Amanda": [ 1135 | "User00179", 1136 | "User00252", 1137 | "User00334" 1138 | ], 1139 | "Monkey": [ 1140 | "User00192", 1141 | "User00212", 1142 | "User00231", 1143 | "User00454", 1144 | "User00667" 1145 | ], 1146 | "School": [ 1147 | "User00207", 1148 | "User00441", 1149 | "User00601", 1150 | "User00734" 1151 | ], 1152 | "Eagles": [ 1153 | "User00215", 1154 | "User00666" 1155 | ], 1156 | "Shadow": [ 1157 | "User00219", 1158 | "User00425", 1159 | "User00893" 1160 | ], 1161 | "michelle": [ 1162 | "User00233", 1163 | "User00760", 1164 | "User00968" 1165 | ], 1166 | "dragon": [ 1167 | "User00235", 1168 | "User00399" 1169 | ], 1170 | "michael": [ 1171 | "User00241", 1172 | "User00407", 1173 | "User00516", 1174 | "User00749", 1175 | "User00801" 1176 | ], 1177 | "Hatton": [ 1178 | "User00256", 1179 | "User00308", 1180 | "User00389", 1181 | "User00650", 1182 | "User00671", 1183 | "User00894", 1184 | "User00982" 1185 | ], 1186 | "diamond": [ 1187 | "User00293", 1188 | "User00826" 1189 | ], 1190 | "daniel": [ 1191 | "User00304", 1192 | "User00503" 1193 | ], 1194 | "pascal": [ 1195 | "User00357", 1196 | "User00495", 1197 | "User00865" 1198 | ], 1199 | "Nicole": [ 1200 | "User00380", 1201 | "User00973" 1202 | ], 1203 | "Soccer": [ 1204 | "User00394", 1205 | "User00416", 1206 | "User00772", 1207 | "User00811" 1208 | ], 1209 | "123abc": [ 1210 | "User00470", 1211 | "User00535" 1212 | ], 1213 | "Snoopy": [ 1214 | "User00476", 1215 | "User00655" 1216 | ], 1217 | "Michael": [ 1218 | "User00488", 1219 | "User00573", 1220 | "User00588", 1221 | "User00765", 1222 | "User00831", 1223 | "User00949" 1224 | ], 1225 | "Vikings": [ 1226 | "User00506", 1227 | "User00887" 1228 | ], 1229 | "maggie": [ 1230 | "User00520", 1231 | "User00527" 1232 | ], 1233 | "harley": [ 1234 | "User00714", 1235 | "User00997" 1236 | ], 1237 | "Smokey": [ 1238 | "User00885", 1239 | "User00969" 1240 | ] 1241 | }, 1242 | "user_equals_password": [ 1243 | "User01314" 1244 | ], 1245 | "user_similarto_password": [ 1246 | "User01312" 1247 | ], 1248 | "short_password": { 1249 | "0": [ 1250 | "User00025", 1251 | "User00035", 1252 | "User00040", 1253 | "User00052", 1254 | "User00075", 1255 | "User00097", 1256 | "User00109", 1257 | "User00112", 1258 | "User00117", 1259 | "User00124", 1260 | "User00125", 1261 | "User00132", 1262 | "User00134", 1263 | "User00141", 1264 | "User00142", 1265 | "User00152", 1266 | "User00169", 1267 | "User00172", 1268 | "User00186", 1269 | "User00199", 1270 | "User00217", 1271 | "User00225", 1272 | "User00228", 1273 | "User00234", 1274 | "User00237", 1275 | "User00248", 1276 | "User00249", 1277 | "User00255", 1278 | "User00266", 1279 | "User00273", 1280 | "User00282", 1281 | "User00285", 1282 | "User00289", 1283 | "User00306", 1284 | "User00307", 1285 | "User00309", 1286 | "User00310", 1287 | "User00324", 1288 | "User00333", 1289 | "User00360", 1290 | "User00368", 1291 | "User00369", 1292 | "User00371", 1293 | "User00373", 1294 | "User00374", 1295 | "User00402", 1296 | "User00406", 1297 | "User00412", 1298 | "User00417", 1299 | "User00418", 1300 | "User00429", 1301 | "User00432", 1302 | "User00465", 1303 | "User00466", 1304 | "User00474", 1305 | "User00487", 1306 | "User00490", 1307 | "User00493", 1308 | "User00497", 1309 | "User00498", 1310 | "User00500", 1311 | "User00502", 1312 | "User00505", 1313 | "User00512", 1314 | "User00514", 1315 | "User00531", 1316 | "User00542", 1317 | "User00544", 1318 | "User00545", 1319 | "User00546", 1320 | "User00553", 1321 | "User00557", 1322 | "User00583", 1323 | "User00586", 1324 | "User00602", 1325 | "User00607", 1326 | "User00613", 1327 | "User00648", 1328 | "User00651", 1329 | "User00653", 1330 | "User00656", 1331 | "User00658", 1332 | "User00663", 1333 | "User00664", 1334 | "User00694", 1335 | "User00697", 1336 | "User00699", 1337 | "User00721", 1338 | "User00723", 1339 | "User00733", 1340 | "User00739", 1341 | "User00740", 1342 | "User00753", 1343 | "User00755", 1344 | "User00758", 1345 | "User00763", 1346 | "User00764", 1347 | "User00769", 1348 | "User00773", 1349 | "User00793", 1350 | "User00823", 1351 | "User00828", 1352 | "User00847", 1353 | "User00862", 1354 | "User00863", 1355 | "User00877", 1356 | "User00879", 1357 | "User00880", 1358 | "User00883", 1359 | "User00884", 1360 | "User00902", 1361 | "User00906", 1362 | "User00916", 1363 | "User00922", 1364 | "User00932", 1365 | "User00934", 1366 | "User00935", 1367 | "User00957", 1368 | "User00959", 1369 | "User00983", 1370 | "User00993", 1371 | "User00995", 1372 | "User01300", 1373 | "User01301", 1374 | "User01302", 1375 | "User01304", 1376 | "User01305", 1377 | "User01306", 1378 | "User01307", 1379 | "User01308", 1380 | "User01309", 1381 | "User01311" 1382 | ], 1383 | "1": [], 1384 | "2": [], 1385 | "3": [ 1386 | "User00060", 1387 | "User00086", 1388 | "User00108", 1389 | "User00213", 1390 | "User00359", 1391 | "User00836", 1392 | "User00984" 1393 | ], 1394 | "4": [ 1395 | "User00001", 1396 | "User00013", 1397 | "User00016", 1398 | "User00076", 1399 | "User00106", 1400 | "User00114", 1401 | "User00123", 1402 | "User00130", 1403 | "User00206", 1404 | "User00254", 1405 | "User00290", 1406 | "User00387", 1407 | "User00400", 1408 | "User00408", 1409 | "User00452", 1410 | "User00462", 1411 | "User00539", 1412 | "User00566", 1413 | "User00570", 1414 | "User00595", 1415 | "User00597", 1416 | "User00610", 1417 | "User00611", 1418 | "User00684", 1419 | "User00735", 1420 | "User00771", 1421 | "User00829", 1422 | "User00837", 1423 | "User01312" 1424 | ], 1425 | "5": [ 1426 | "User00006", 1427 | "User00022", 1428 | "User00056", 1429 | "User00070", 1430 | "User00071", 1431 | "User00080", 1432 | "User00094", 1433 | "User00095", 1434 | "User00104", 1435 | "User00110", 1436 | "User00127", 1437 | "User00128", 1438 | "User00139", 1439 | "User00148", 1440 | "User00149", 1441 | "User00159", 1442 | "User00202", 1443 | "User00214", 1444 | "User00229", 1445 | "User00242", 1446 | "User00267", 1447 | "User00274", 1448 | "User00283", 1449 | "User00301", 1450 | "User00321", 1451 | "User00330", 1452 | "User00336", 1453 | "User00340", 1454 | "User00343", 1455 | "User00348", 1456 | "User00358", 1457 | "User00361", 1458 | "User00363", 1459 | "User00377", 1460 | "User00378", 1461 | "User00382", 1462 | "User00403", 1463 | "User00414", 1464 | "User00424", 1465 | "User00431", 1466 | "User00435", 1467 | "User00439", 1468 | "User00446", 1469 | "User00450", 1470 | "User00453", 1471 | "User00459", 1472 | "User00460", 1473 | "User00469", 1474 | "User00477", 1475 | "User00481", 1476 | "User00494", 1477 | "User00511", 1478 | "User00517", 1479 | "User00518", 1480 | "User00521", 1481 | "User00529", 1482 | "User00533", 1483 | "User00534", 1484 | "User00543", 1485 | "User00550", 1486 | "User00578", 1487 | "User00591", 1488 | "User00600", 1489 | "User00619", 1490 | "User00620", 1491 | "User00630", 1492 | "User00635", 1493 | "User00639", 1494 | "User00673", 1495 | "User00676", 1496 | "User00683", 1497 | "User00692", 1498 | "User00698", 1499 | "User00701", 1500 | "User00712", 1501 | "User00722", 1502 | "User00725", 1503 | "User00744", 1504 | "User00762", 1505 | "User00767", 1506 | "User00774", 1507 | "User00776", 1508 | "User00791", 1509 | "User00792", 1510 | "User00802", 1511 | "User00804", 1512 | "User00818", 1513 | "User00824", 1514 | "User00830", 1515 | "User00841", 1516 | "User00842", 1517 | "User00876", 1518 | "User00891", 1519 | "User00907", 1520 | "User00911", 1521 | "User00918", 1522 | "User00921", 1523 | "User00927", 1524 | "User00937", 1525 | "User00946", 1526 | "User00962", 1527 | "User00979", 1528 | "User00980", 1529 | "User00990", 1530 | "User00994", 1531 | "User00996" 1532 | ] 1533 | }, 1534 | "cracked_computer_accounts": [] 1535 | } 1536 | } 1537 | -------------------------------------------------------------------------------- /tests/hash.txt.out: -------------------------------------------------------------------------------- 1 | contoso.local\User00000:Purple 2 | contoso.local\User00001:test 3 | contoso.local\User00002:123456 4 | contoso.local\User00003:abc123 5 | contoso.local\User00004:Hockey 6 | contoso.local\User00005:123456 7 | contoso.local\User00006:ou812 8 | contoso.local\User00007:password 9 | contoso.local\User00008:notused 10 | contoso.local\User00009:Maddock 11 | contoso.local\User00010:mickey 12 | contoso.local\User00011:Hockey 13 | contoso.local\User00012:password 14 | contoso.local\User00013:test 15 | contoso.local\User00014:newuser 16 | contoso.local\User00015:abc123 17 | contoso.local\User00016:0246 18 | contoso.local\User00017:1q2w3e 19 | contoso.local\User00018:Beavis 20 | contoso.local\User00019:Justin 21 | contoso.local\User00020:password 22 | contoso.local\User00021:Cowboys 23 | contoso.local\User00022:money 24 | contoso.local\User00023:Mickey 25 | contoso.local\User00024:notused 26 | contoso.local\User00025: 27 | contoso.local\User00026:newpass 28 | contoso.local\User00027:12345678 29 | contoso.local\User00028:a1b2c3 30 | contoso.local\User00029:Jessica 31 | contoso.local\User00030:notused 32 | contoso.local\User00031:wheeling 33 | contoso.local\User00032:abc123 34 | contoso.local\User00033:Beavis 35 | contoso.local\User00034:internet 36 | contoso.local\User00035: 37 | contoso.local\User00036:Cowboys 38 | contoso.local\User00037:123456 39 | contoso.local\User00038:tigger 40 | contoso.local\User00039:Internet 41 | contoso.local\User00040: 42 | contoso.local\User00041:notused 43 | contoso.local\User00042:mustang 44 | contoso.local\User00043:abc123 45 | contoso.local\User00044:passwd 46 | contoso.local\User00045:mustang 47 | contoso.local\User00046:abc123 48 | contoso.local\User00047:Maddock 49 | contoso.local\User00048:abc123 50 | contoso.local\User00049:Jessica 51 | contoso.local\User00050:Beavis 52 | contoso.local\User00051:newpass 53 | contoso.local\User00052: 54 | contoso.local\User00053:password 55 | contoso.local\User00054:Hockey 56 | contoso.local\User00055:jordan 57 | contoso.local\User00056:12345 58 | contoso.local\User00057:internet 59 | contoso.local\User00058:fiction 60 | contoso.local\User00059:internet 61 | contoso.local\User00060:123 62 | contoso.local\User00061:internet 63 | contoso.local\User00062:patrick 64 | contoso.local\User00063:12345678 65 | contoso.local\User00064:Maddock 66 | contoso.local\User00065:newpass 67 | contoso.local\User00066:12345678 68 | contoso.local\User00067:123456 69 | contoso.local\User00068:Maddock 70 | contoso.local\User00069:password 71 | contoso.local\User00070:12345 72 | contoso.local\User00071:12345 73 | contoso.local\User00072:newuser 74 | contoso.local\User00073:Internet 75 | contoso.local\User00074:notused 76 | contoso.local\User00075: 77 | contoso.local\User00076:test 78 | contoso.local\User00077:Qwerty 79 | contoso.local\User00078:foobar 80 | contoso.local\User00079:Purple 81 | contoso.local\User00080:money 82 | contoso.local\User00081:foobar 83 | contoso.local\User00082:internet 84 | contoso.local\User00083:123456 85 | contoso.local\User00084:fiction 86 | contoso.local\User00085:password 87 | contoso.local\User00086:123 88 | contoso.local\User00087:Sports 89 | contoso.local\User00088:jennifer 90 | contoso.local\User00089:12345678 91 | contoso.local\User00090:Dakota 92 | contoso.local\User00091:Maddock 93 | contoso.local\User00092:newpass 94 | contoso.local\User00093:Internet 95 | contoso.local\User00094:!@#$% 96 | contoso.local\User00095:chris 97 | contoso.local\User00096:orange 98 | contoso.local\User00097: 99 | contoso.local\User00098:newuser 100 | contoso.local\User00099:notused 101 | contoso.local\User00100:newuser 102 | contoso.local\User00101:Sports 103 | contoso.local\User00102:abc123 104 | contoso.local\User00103:Jordan 105 | contoso.local\User00104:12345 106 | contoso.local\User00105:abc123 107 | contoso.local\User00106:1234 108 | contoso.local\User00107:passwd 109 | contoso.local\User00108:123 110 | contoso.local\User00109: 111 | contoso.local\User00110:12345 112 | contoso.local\User00111:qwerty 113 | contoso.local\User00112: 114 | contoso.local\User00113:Maddock 115 | contoso.local\User00114:0246 116 | contoso.local\User00115:internet 117 | contoso.local\User00116:Mickey 118 | contoso.local\User00117: 119 | contoso.local\User00118:computer 120 | contoso.local\User00119:12345678 121 | contoso.local\User00120:Maddock 122 | contoso.local\User00121:123456 123 | contoso.local\User00122:passwd 124 | contoso.local\User00123:mike 125 | contoso.local\User00124: 126 | contoso.local\User00125: 127 | contoso.local\User00126:monday 128 | contoso.local\User00127:12345 129 | contoso.local\User00128:12345 130 | contoso.local\User00129:abc123 131 | contoso.local\User00130:1234 132 | contoso.local\User00131:clever 133 | contoso.local\User00132: 134 | contoso.local\User00133:newpass 135 | contoso.local\User00134: 136 | contoso.local\User00135:abc123 137 | contoso.local\User00136:abc123 138 | contoso.local\User00137:computer 139 | contoso.local\User00138:passwd 140 | contoso.local\User00139:12345 141 | contoso.local\User00140:Hockey 142 | contoso.local\User00141: 143 | contoso.local\User00142: 144 | contoso.local\User00143:bandit 145 | contoso.local\User00144:abc123 146 | contoso.local\User00145:passwd 147 | contoso.local\User00146:Jordan 148 | contoso.local\User00147:shadow 149 | contoso.local\User00148:david 150 | contoso.local\User00149:12345 151 | contoso.local\User00150:Mickey 152 | contoso.local\User00151:Dallas 153 | contoso.local\User00152: 154 | contoso.local\User00153:Mickey 155 | contoso.local\User00154:Sports 156 | contoso.local\User00155:passwd 157 | contoso.local\User00156:abc123 158 | contoso.local\User00157:12345678 159 | contoso.local\User00158:newpass 160 | contoso.local\User00159:12345 161 | contoso.local\User00160:notused 162 | contoso.local\User00161:123456 163 | contoso.local\User00162:Maddock 164 | contoso.local\User00163:Hockey 165 | contoso.local\User00164:password 166 | contoso.local\User00165:passwd 167 | contoso.local\User00166:shadow 168 | contoso.local\User00167:abc123 169 | contoso.local\User00168:internet 170 | contoso.local\User00169: 171 | contoso.local\User00170:Jordan 172 | contoso.local\User00171:passwd 173 | contoso.local\User00172: 174 | contoso.local\User00173:password 175 | contoso.local\User00174:abc123 176 | contoso.local\User00175:sunshine 177 | contoso.local\User00176:passwd 178 | contoso.local\User00177:password 179 | contoso.local\User00178:passwd 180 | contoso.local\User00179:Amanda 181 | contoso.local\User00180:computer 182 | contoso.local\User00181:newuser 183 | contoso.local\User00182:fiction 184 | contoso.local\User00183:123456 185 | contoso.local\User00184:123456 186 | contoso.local\User00185:computer 187 | contoso.local\User00186: 188 | contoso.local\User00187:abc123 189 | contoso.local\User00188:passwd 190 | contoso.local\User00189:abc123 191 | contoso.local\User00190:computer 192 | contoso.local\User00191:123456 193 | contoso.local\User00192:Monkey 194 | contoso.local\User00193:Maddock 195 | contoso.local\User00194:123456 196 | contoso.local\User00195:123456 197 | contoso.local\User00196:qwerty 198 | contoso.local\User00197:123456 199 | contoso.local\User00198:Internet 200 | contoso.local\User00199: 201 | contoso.local\User00200:newpass 202 | contoso.local\User00201:abc123 203 | contoso.local\User00202:12345 204 | contoso.local\User00203:Hockey 205 | contoso.local\User00204:notused 206 | contoso.local\User00205:internet 207 | contoso.local\User00206:test 208 | contoso.local\User00207:School 209 | contoso.local\User00208:notused 210 | contoso.local\User00209:password 211 | contoso.local\User00210:notused 212 | contoso.local\User00211:Beavis 213 | contoso.local\User00212:Monkey 214 | contoso.local\User00213:123 215 | contoso.local\User00214:12345 216 | contoso.local\User00215:Eagles 217 | contoso.local\User00216:computer 218 | contoso.local\User00217: 219 | contoso.local\User00218:Cowboys 220 | contoso.local\User00219:Shadow 221 | contoso.local\User00220:123456 222 | contoso.local\User00221:Mickey 223 | contoso.local\User00222:password 224 | contoso.local\User00223:abc123 225 | contoso.local\User00224:Cowboys 226 | contoso.local\User00225: 227 | contoso.local\User00226:tigger 228 | contoso.local\User00227:Hockey 229 | contoso.local\User00228: 230 | contoso.local\User00229:12345 231 | contoso.local\User00230:computer 232 | contoso.local\User00231:Monkey 233 | contoso.local\User00232:password 234 | contoso.local\User00233:michelle 235 | contoso.local\User00234: 236 | contoso.local\User00235:dragon 237 | contoso.local\User00236:123456 238 | contoso.local\User00237: 239 | contoso.local\User00238:password 240 | contoso.local\User00239:newpass 241 | contoso.local\User00240:orange 242 | contoso.local\User00241:michael 243 | contoso.local\User00242:12345 244 | contoso.local\User00243:Hockey 245 | contoso.local\User00244:computer 246 | contoso.local\User00245:passwd 247 | contoso.local\User00246:mustang 248 | contoso.local\User00247:password 249 | contoso.local\User00248: 250 | contoso.local\User00249: 251 | contoso.local\User00250:Mickey 252 | contoso.local\User00251:internet 253 | contoso.local\User00252:Amanda 254 | contoso.local\User00253:clever 255 | contoso.local\User00254:0246 256 | contoso.local\User00255: 257 | contoso.local\User00256:Hatton 258 | contoso.local\User00257:qwerty 259 | contoso.local\User00258:Internet 260 | contoso.local\User00259:fiction 261 | contoso.local\User00260:Justin 262 | contoso.local\User00261:computer 263 | contoso.local\User00262:passwd 264 | contoso.local\User00263:internet 265 | contoso.local\User00264:abc123 266 | contoso.local\User00265:Maddock 267 | contoso.local\User00266: 268 | contoso.local\User00267:12345 269 | contoso.local\User00268:passwd 270 | contoso.local\User00269:orange 271 | contoso.local\User00270:Cowboys 272 | contoso.local\User00271:passwd 273 | contoso.local\User00272:newuser 274 | contoso.local\User00273: 275 | contoso.local\User00274:12345 276 | contoso.local\User00275:silver 277 | contoso.local\User00276:password 278 | contoso.local\User00277:123456 279 | contoso.local\User00278:internet 280 | contoso.local\User00279:password 281 | contoso.local\User00280:notused 282 | contoso.local\User00281:abc123 283 | contoso.local\User00282: 284 | contoso.local\User00283:12345 285 | contoso.local\User00284:Hockey 286 | contoso.local\User00285: 287 | contoso.local\User00286:123456 288 | contoso.local\User00287:passwd 289 | contoso.local\User00288:123456 290 | contoso.local\User00289: 291 | contoso.local\User00290:0246 292 | contoso.local\User00291:notused 293 | contoso.local\User00292:Purple 294 | contoso.local\User00293:diamond 295 | contoso.local\User00294:passwd 296 | contoso.local\User00295:notused 297 | contoso.local\User00296:qwerty 298 | contoso.local\User00297:newpass 299 | contoso.local\User00298:password 300 | contoso.local\User00299:Jordan 301 | contoso.local\User00300:jennifer 302 | contoso.local\User00301:david 303 | contoso.local\User00302:newuser 304 | contoso.local\User00303:passwd 305 | contoso.local\User00304:daniel 306 | contoso.local\User00305:computer 307 | contoso.local\User00306: 308 | contoso.local\User00307: 309 | contoso.local\User00308:Hatton 310 | contoso.local\User00309: 311 | contoso.local\User00310: 312 | contoso.local\User00311:123456 313 | contoso.local\User00312:Hockey 314 | contoso.local\User00313:newpass 315 | contoso.local\User00314:notused 316 | contoso.local\User00315:Maddock 317 | contoso.local\User00316:Mickey 318 | contoso.local\User00317:123456 319 | contoso.local\User00318:passwd 320 | contoso.local\User00319:bandit 321 | contoso.local\User00320:abc123 322 | contoso.local\User00321:ou812 323 | contoso.local\User00322:newpass 324 | contoso.local\User00323:passwd 325 | contoso.local\User00324: 326 | contoso.local\User00325:wheeling 327 | contoso.local\User00326:qwerty 328 | contoso.local\User00327:Justin 329 | contoso.local\User00328:newpass 330 | contoso.local\User00329:Mickey 331 | contoso.local\User00330:12345 332 | contoso.local\User00331:123456 333 | contoso.local\User00332:abc123 334 | contoso.local\User00333: 335 | contoso.local\User00334:Amanda 336 | contoso.local\User00335:abc123 337 | contoso.local\User00336:12345 338 | contoso.local\User00337:abc123 339 | contoso.local\User00338:123456 340 | contoso.local\User00339:internet 341 | contoso.local\User00340:12345 342 | contoso.local\User00341:bandit 343 | contoso.local\User00342:1q2w3e 344 | contoso.local\User00343:12345 345 | contoso.local\User00344:123456 346 | contoso.local\User00345:Mickey 347 | contoso.local\User00346:Qwerty 348 | contoso.local\User00347:notused 349 | contoso.local\User00348:chris 350 | contoso.local\User00349:password 351 | contoso.local\User00350:notused 352 | contoso.local\User00351:notused 353 | contoso.local\User00352:Internet 354 | contoso.local\User00353:abc123 355 | contoso.local\User00354:12345678 356 | contoso.local\User00355:abc123 357 | contoso.local\User00356:passwd 358 | contoso.local\User00357:pascal 359 | contoso.local\User00358:money 360 | contoso.local\User00359:123 361 | contoso.local\User00360: 362 | contoso.local\User00361:12345 363 | contoso.local\User00362:abc123 364 | contoso.local\User00363:ou812 365 | contoso.local\User00364:Purple 366 | contoso.local\User00365:Maddock 367 | contoso.local\User00366:Cowboys 368 | contoso.local\User00367:Beavis 369 | contoso.local\User00368: 370 | contoso.local\User00369: 371 | contoso.local\User00370:mustang 372 | contoso.local\User00371: 373 | contoso.local\User00372:abc123 374 | contoso.local\User00373: 375 | contoso.local\User00374: 376 | contoso.local\User00375:Purple 377 | contoso.local\User00376:passwd 378 | contoso.local\User00377:12345 379 | contoso.local\User00378:12345 380 | contoso.local\User00379:Internet 381 | contoso.local\User00380:Nicole 382 | contoso.local\User00381:Hockey 383 | contoso.local\User00382:money 384 | contoso.local\User00383:Hockey 385 | contoso.local\User00384:Maddock 386 | contoso.local\User00385:Mickey 387 | contoso.local\User00386:Beavis 388 | contoso.local\User00387:test 389 | contoso.local\User00388:falcon 390 | contoso.local\User00389:Hatton 391 | contoso.local\User00390:mickey 392 | contoso.local\User00391:abc123 393 | contoso.local\User00392:internet 394 | contoso.local\User00393:12345678 395 | contoso.local\User00394:Soccer 396 | contoso.local\User00395:notused 397 | contoso.local\User00396:orange 398 | contoso.local\User00397:123456 399 | contoso.local\User00398:Maddock 400 | contoso.local\User00399:dragon 401 | contoso.local\User00400:1234 402 | contoso.local\User00401:Internet 403 | contoso.local\User00402: 404 | contoso.local\User00403:12345 405 | contoso.local\User00404:Hockey 406 | contoso.local\User00405:abc123 407 | contoso.local\User00406: 408 | contoso.local\User00407:michael 409 | contoso.local\User00408:fred 410 | contoso.local\User00409:password 411 | contoso.local\User00410:123456 412 | contoso.local\User00411:Beavis 413 | contoso.local\User00412: 414 | contoso.local\User00413:computer 415 | contoso.local\User00414:12345 416 | contoso.local\User00415:password 417 | contoso.local\User00416:Soccer 418 | contoso.local\User00417: 419 | contoso.local\User00418: 420 | contoso.local\User00419:123456 421 | contoso.local\User00420:notused 422 | contoso.local\User00421:wheeling 423 | contoso.local\User00422:Cowboys 424 | contoso.local\User00423:Mickey 425 | contoso.local\User00424:12345 426 | contoso.local\User00425:Shadow 427 | contoso.local\User00426:123456 428 | contoso.local\User00427:newuser 429 | contoso.local\User00428:123456 430 | contoso.local\User00429: 431 | contoso.local\User00430:notused 432 | contoso.local\User00431:12345 433 | contoso.local\User00432: 434 | contoso.local\User00433:newuser 435 | contoso.local\User00434:newuser 436 | contoso.local\User00435:12345 437 | contoso.local\User00436:internet 438 | contoso.local\User00437:passwd 439 | contoso.local\User00438:123456 440 | contoso.local\User00439:ou812 441 | contoso.local\User00440:notused 442 | contoso.local\User00441:School 443 | contoso.local\User00442:12345678 444 | contoso.local\User00443:notused 445 | contoso.local\User00444:newuser 446 | contoso.local\User00445:passwd 447 | contoso.local\User00446:12345 448 | contoso.local\User00447:Mickey 449 | contoso.local\User00448:newuser 450 | contoso.local\User00449:orange 451 | contoso.local\User00450:12345 452 | contoso.local\User00451:a1b2c3 453 | contoso.local\User00452:1234 454 | contoso.local\User00453:chris 455 | contoso.local\User00454:Monkey 456 | contoso.local\User00455:Mickey 457 | contoso.local\User00456:Beavis 458 | contoso.local\User00457:Hockey 459 | contoso.local\User00458:orange 460 | contoso.local\User00459:12345 461 | contoso.local\User00460:ou812 462 | contoso.local\User00461:newpass 463 | contoso.local\User00462:test 464 | contoso.local\User00463:newpass 465 | contoso.local\User00464:orange 466 | contoso.local\User00465: 467 | contoso.local\User00466: 468 | contoso.local\User00467:jordan 469 | contoso.local\User00468:notused 470 | contoso.local\User00469:money 471 | contoso.local\User00470:123abc 472 | contoso.local\User00471:Internet 473 | contoso.local\User00472:Maddock 474 | contoso.local\User00473:abc123 475 | contoso.local\User00474: 476 | contoso.local\User00475:tigger 477 | contoso.local\User00476:Snoopy 478 | contoso.local\User00477:ou812 479 | contoso.local\User00478:qwerty 480 | contoso.local\User00479:123456 481 | contoso.local\User00480:password 482 | contoso.local\User00481:12345 483 | contoso.local\User00482:newpass 484 | contoso.local\User00483:Maddock 485 | contoso.local\User00484:foobar 486 | contoso.local\User00485:stupid 487 | contoso.local\User00486:123456 488 | contoso.local\User00487: 489 | contoso.local\User00488:Michael 490 | contoso.local\User00489:Jordan 491 | contoso.local\User00490: 492 | contoso.local\User00491:newpass 493 | contoso.local\User00492:Mickey 494 | contoso.local\User00493: 495 | contoso.local\User00494:david 496 | contoso.local\User00495:pascal 497 | contoso.local\User00496:Cowboys 498 | contoso.local\User00497: 499 | contoso.local\User00498: 500 | contoso.local\User00499:passwd 501 | contoso.local\User00500: 502 | contoso.local\User00501:passwd 503 | contoso.local\User00502: 504 | contoso.local\User00503:daniel 505 | contoso.local\User00504:tigger 506 | contoso.local\User00505: 507 | contoso.local\User00506:Vikings 508 | contoso.local\User00507:abc123 509 | contoso.local\User00508:abc123 510 | contoso.local\User00509:Mickey 511 | contoso.local\User00510:clever 512 | contoso.local\User00511:12345 513 | contoso.local\User00512: 514 | contoso.local\User00513:passwd 515 | contoso.local\User00514: 516 | contoso.local\User00515:12345678 517 | contoso.local\User00516:michael 518 | contoso.local\User00517:12345 519 | contoso.local\User00518:12345 520 | contoso.local\User00519:passwd 521 | contoso.local\User00520:maggie 522 | contoso.local\User00521:12345 523 | contoso.local\User00522:Dallas 524 | contoso.local\User00523:passwd 525 | contoso.local\User00524:notused 526 | contoso.local\User00525:password 527 | contoso.local\User00526:clever 528 | contoso.local\User00527:maggie 529 | contoso.local\User00528:computer 530 | contoso.local\User00529:12345 531 | contoso.local\User00530:richard 532 | contoso.local\User00531: 533 | contoso.local\User00532:Maddock 534 | contoso.local\User00533:12345 535 | contoso.local\User00534:money 536 | contoso.local\User00535:123abc 537 | contoso.local\User00536:Beavis 538 | contoso.local\User00537:password 539 | contoso.local\User00538:abc123 540 | contoso.local\User00539:test 541 | contoso.local\User00540:password 542 | contoso.local\User00541:123456 543 | contoso.local\User00542: 544 | contoso.local\User00543:12345 545 | contoso.local\User00544: 546 | contoso.local\User00545: 547 | contoso.local\User00546: 548 | contoso.local\User00547:123456 549 | contoso.local\User00548:internet 550 | contoso.local\User00549:Jordan 551 | contoso.local\User00550:12345 552 | contoso.local\User00551:123456 553 | contoso.local\User00552:computer 554 | contoso.local\User00553: 555 | contoso.local\User00554:newpass 556 | contoso.local\User00555:passwd 557 | contoso.local\User00556:Dakota 558 | contoso.local\User00557: 559 | contoso.local\User00558:123456 560 | contoso.local\User00559:123456 561 | contoso.local\User00560:Cowboys 562 | contoso.local\User00561:password 563 | contoso.local\User00562:abc123 564 | contoso.local\User00563:password 565 | contoso.local\User00564:notused 566 | contoso.local\User00565:Dakota 567 | contoso.local\User00566:1234 568 | contoso.local\User00567:password 569 | contoso.local\User00568:internet 570 | contoso.local\User00569:qwerty 571 | contoso.local\User00570:1234 572 | contoso.local\User00571:newpass 573 | contoso.local\User00572:123456 574 | contoso.local\User00573:Michael 575 | contoso.local\User00574:newpass 576 | contoso.local\User00575:abc123 577 | contoso.local\User00576:newpass 578 | contoso.local\User00577:clever 579 | contoso.local\User00578:12345 580 | contoso.local\User00579:newpass 581 | contoso.local\User00580:computer 582 | contoso.local\User00581:notused 583 | contoso.local\User00582:abc123 584 | contoso.local\User00583: 585 | contoso.local\User00584:qwerty 586 | contoso.local\User00585:newuser 587 | contoso.local\User00586: 588 | contoso.local\User00587:password 589 | contoso.local\User00588:Michael 590 | contoso.local\User00589:newuser 591 | contoso.local\User00590:internet 592 | contoso.local\User00591:12345 593 | contoso.local\User00592:fiction 594 | contoso.local\User00593:notused 595 | contoso.local\User00594:Mickey 596 | contoso.local\User00595:test 597 | contoso.local\User00596:abc123 598 | contoso.local\User00597:test 599 | contoso.local\User00598:password 600 | contoso.local\User00599:Friends 601 | contoso.local\User00600:12345 602 | contoso.local\User00601:School 603 | contoso.local\User00602: 604 | contoso.local\User00603:abc123 605 | contoso.local\User00604:Hockey 606 | contoso.local\User00605:Maddock 607 | contoso.local\User00606:123456 608 | contoso.local\User00607: 609 | contoso.local\User00608:abc123 610 | contoso.local\User00609:password 611 | contoso.local\User00610:test 612 | contoso.local\User00611:0246 613 | contoso.local\User00612:12345678 614 | contoso.local\User00613: 615 | contoso.local\User00614:orange 616 | contoso.local\User00615:password 617 | contoso.local\User00616:passwd 618 | contoso.local\User00617:Hockey 619 | contoso.local\User00618:abc123 620 | contoso.local\User00619:david 621 | contoso.local\User00620:12345 622 | contoso.local\User00621:123456 623 | contoso.local\User00622:password 624 | contoso.local\User00623:Hockey 625 | contoso.local\User00624:Maddock 626 | contoso.local\User00625:newuser 627 | contoso.local\User00626:Hockey 628 | contoso.local\User00627:123456 629 | contoso.local\User00628:computer 630 | contoso.local\User00629:123456 631 | contoso.local\User00630:ou812 632 | contoso.local\User00631:orange 633 | contoso.local\User00632:password 634 | contoso.local\User00633:passwd 635 | contoso.local\User00634:newuser 636 | contoso.local\User00635:12345 637 | contoso.local\User00636:qwerty 638 | contoso.local\User00637:123456 639 | contoso.local\User00638:12345678 640 | contoso.local\User00639:12345 641 | contoso.local\User00640:Internet 642 | contoso.local\User00641:passwd 643 | contoso.local\User00642:notused 644 | contoso.local\User00643:123456 645 | contoso.local\User00644:notused 646 | contoso.local\User00645:qwerty 647 | contoso.local\User00646:password 648 | contoso.local\User00647:computer 649 | contoso.local\User00648: 650 | contoso.local\User00649:password 651 | contoso.local\User00650:Hatton 652 | contoso.local\User00651: 653 | contoso.local\User00652:newuser 654 | contoso.local\User00653: 655 | contoso.local\User00654:notused 656 | contoso.local\User00655:Snoopy 657 | contoso.local\User00656: 658 | contoso.local\User00657:Hockey 659 | contoso.local\User00658: 660 | contoso.local\User00659:mustang 661 | contoso.local\User00660:Mickey 662 | contoso.local\User00661:Jordan 663 | contoso.local\User00662:Internet 664 | contoso.local\User00663: 665 | contoso.local\User00664: 666 | contoso.local\User00665:newuser 667 | contoso.local\User00666:Eagles 668 | contoso.local\User00667:Monkey 669 | contoso.local\User00668:qwerty 670 | contoso.local\User00669:Cowboys 671 | contoso.local\User00670:abc123 672 | contoso.local\User00671:Hatton 673 | contoso.local\User00672:password 674 | contoso.local\User00673:12345 675 | contoso.local\User00674:newpass 676 | contoso.local\User00675:passwd 677 | contoso.local\User00676:12345 678 | contoso.local\User00677:123456 679 | contoso.local\User00678:jennifer 680 | contoso.local\User00679:newpass 681 | contoso.local\User00680:newpass 682 | contoso.local\User00681:fiction 683 | contoso.local\User00682:Hockey 684 | contoso.local\User00683:12345 685 | contoso.local\User00684:0246 686 | contoso.local\User00685:newpass 687 | contoso.local\User00686:orange 688 | contoso.local\User00687:123456 689 | contoso.local\User00688:Beavis 690 | contoso.local\User00689:123456 691 | contoso.local\User00690:abc123 692 | contoso.local\User00691:abc123 693 | contoso.local\User00692:apple 694 | contoso.local\User00693:Maddock 695 | contoso.local\User00694: 696 | contoso.local\User00695:123456 697 | contoso.local\User00696:abc123 698 | contoso.local\User00697: 699 | contoso.local\User00698:12345 700 | contoso.local\User00699: 701 | contoso.local\User00700:computer 702 | contoso.local\User00701:12345 703 | contoso.local\User00702:12345678 704 | contoso.local\User00703:newuser 705 | contoso.local\User00704:fiction 706 | contoso.local\User00705:Hockey 707 | contoso.local\User00706:password 708 | contoso.local\User00707:Qwerty 709 | contoso.local\User00708:passwd 710 | contoso.local\User00709:newpass 711 | contoso.local\User00710:newuser 712 | contoso.local\User00711:Hockey 713 | contoso.local\User00712:hello 714 | contoso.local\User00713:andrew 715 | contoso.local\User00714:harley 716 | contoso.local\User00715:jordan 717 | contoso.local\User00716:Maddock 718 | contoso.local\User00717:notused 719 | contoso.local\User00718:fiction 720 | contoso.local\User00719:abc123 721 | contoso.local\User00720:password 722 | contoso.local\User00721: 723 | contoso.local\User00722:12345 724 | contoso.local\User00723: 725 | contoso.local\User00724:Internet 726 | contoso.local\User00725:12345 727 | contoso.local\User00726:orange 728 | contoso.local\User00727:Mickey 729 | contoso.local\User00728:Sendit 730 | contoso.local\User00729:123456 731 | contoso.local\User00730:foobar 732 | contoso.local\User00731:newpass 733 | contoso.local\User00732:passwd 734 | contoso.local\User00733: 735 | contoso.local\User00734:School 736 | contoso.local\User00735:1234 737 | contoso.local\User00736:password 738 | contoso.local\User00737:newpass 739 | contoso.local\User00738:passwd 740 | contoso.local\User00739: 741 | contoso.local\User00740: 742 | contoso.local\User00741:passwd 743 | contoso.local\User00742:abc123 744 | contoso.local\User00743:Hockey 745 | contoso.local\User00744:12345 746 | contoso.local\User00745:abc123 747 | contoso.local\User00746:Mickey 748 | contoso.local\User00747:Jordan 749 | contoso.local\User00748:password 750 | contoso.local\User00749:michael 751 | contoso.local\User00750:abc123 752 | contoso.local\User00751:passwd 753 | contoso.local\User00752:fiction 754 | contoso.local\User00753: 755 | contoso.local\User00754:jordan 756 | contoso.local\User00755: 757 | contoso.local\User00756:newpass 758 | contoso.local\User00757:Cowboys 759 | contoso.local\User00758: 760 | contoso.local\User00759:Beavis 761 | contoso.local\User00760:michelle 762 | contoso.local\User00761:newpass 763 | contoso.local\User00762:12345 764 | contoso.local\User00763: 765 | contoso.local\User00764: 766 | contoso.local\User00765:Michael 767 | contoso.local\User00766:123456 768 | contoso.local\User00767:david 769 | contoso.local\User00768:newpass 770 | contoso.local\User00769: 771 | contoso.local\User00770:newpass 772 | contoso.local\User00771:test 773 | contoso.local\User00772:Soccer 774 | contoso.local\User00773: 775 | contoso.local\User00774:12345 776 | contoso.local\User00775:notused 777 | contoso.local\User00776:12345 778 | contoso.local\User00777:mustang 779 | contoso.local\User00778:jordan 780 | contoso.local\User00779:password 781 | contoso.local\User00780:newpass 782 | contoso.local\User00781:internet 783 | contoso.local\User00782:password 784 | contoso.local\User00783:internet 785 | contoso.local\User00784:abc123 786 | contoso.local\User00785:password 787 | contoso.local\User00786:newpass 788 | contoso.local\User00787:12345678 789 | contoso.local\User00788:internet 790 | contoso.local\User00789:abc123 791 | contoso.local\User00790:abc123 792 | contoso.local\User00791:12345 793 | contoso.local\User00792:ou812 794 | contoso.local\User00793: 795 | contoso.local\User00794:Jordan 796 | contoso.local\User00795:abc123 797 | contoso.local\User00796:password 798 | contoso.local\User00797:Jordan 799 | contoso.local\User00798:Internet 800 | contoso.local\User00799:Jordan 801 | contoso.local\User00800:Hockey 802 | contoso.local\User00801:michael 803 | contoso.local\User00802:12345 804 | contoso.local\User00803:computer 805 | contoso.local\User00804:12345 806 | contoso.local\User00805:Jordan 807 | contoso.local\User00806:passwd 808 | contoso.local\User00807:Mickey 809 | contoso.local\User00808:fiction 810 | contoso.local\User00809:Internet 811 | contoso.local\User00810:abc123 812 | contoso.local\User00811:Soccer 813 | contoso.local\User00812:abc123 814 | contoso.local\User00813:Hockey 815 | contoso.local\User00814:Mickey 816 | contoso.local\User00815:abc123 817 | contoso.local\User00816:newuser 818 | contoso.local\User00817:password 819 | contoso.local\User00818:12345 820 | contoso.local\User00819:Beavis 821 | contoso.local\User00820:internet 822 | contoso.local\User00821:passwd 823 | contoso.local\User00822:Maddock 824 | contoso.local\User00823: 825 | contoso.local\User00824:12345 826 | contoso.local\User00825:123456 827 | contoso.local\User00826:diamond 828 | contoso.local\User00827:Hockey 829 | contoso.local\User00828: 830 | contoso.local\User00829:test 831 | contoso.local\User00830:ou812 832 | contoso.local\User00831:Michael 833 | contoso.local\User00832:Maddock 834 | contoso.local\User00833:qwerty 835 | contoso.local\User00834:abc123 836 | contoso.local\User00835:Jordan 837 | contoso.local\User00836:123 838 | contoso.local\User00837:test 839 | contoso.local\User00838:passwd 840 | contoso.local\User00839:12345678 841 | contoso.local\User00840:newuser 842 | contoso.local\User00841:money 843 | contoso.local\User00842:12345 844 | contoso.local\User00843:shadow 845 | contoso.local\User00844:Hockey 846 | contoso.local\User00845:passwd 847 | contoso.local\User00846:newpass 848 | contoso.local\User00847: 849 | contoso.local\User00848:abc123 850 | contoso.local\User00849:Internet 851 | contoso.local\User00850:Internet 852 | contoso.local\User00851:notused 853 | contoso.local\User00852:sunshine 854 | contoso.local\User00853:notused 855 | contoso.local\User00854:password 856 | contoso.local\User00855:Sports 857 | contoso.local\User00856:abc123 858 | contoso.local\User00857:12345678 859 | contoso.local\User00858:fiction 860 | contoso.local\User00859:tigger 861 | contoso.local\User00860:orange 862 | contoso.local\User00861:123456 863 | contoso.local\User00862: 864 | contoso.local\User00863: 865 | contoso.local\User00864:Cowboys 866 | contoso.local\User00865:pascal 867 | contoso.local\User00866:passwd 868 | contoso.local\User00867:newpass 869 | contoso.local\User00868:Cowboys 870 | contoso.local\User00869:Internet 871 | contoso.local\User00870:abc123 872 | contoso.local\User00871:Mickey 873 | contoso.local\User00872:password 874 | contoso.local\User00873:passwd 875 | contoso.local\User00874:internet 876 | contoso.local\User00875:Jordan 877 | contoso.local\User00876:12345 878 | contoso.local\User00877: 879 | contoso.local\User00878:newuser 880 | contoso.local\User00879: 881 | contoso.local\User00880: 882 | contoso.local\User00881:abc123 883 | contoso.local\User00882:Internet 884 | contoso.local\User00883: 885 | contoso.local\User00884: 886 | contoso.local\User00885:Smokey 887 | contoso.local\User00886:123456 888 | contoso.local\User00887:Vikings 889 | contoso.local\User00888:abc123 890 | contoso.local\User00889:newpass 891 | contoso.local\User00890:passwd 892 | contoso.local\User00891:12345 893 | contoso.local\User00892:passwd 894 | contoso.local\User00893:Shadow 895 | contoso.local\User00894:Hatton 896 | contoso.local\User00895:123456 897 | contoso.local\User00896:123456 898 | contoso.local\User00897:password 899 | contoso.local\User00898:Hockey 900 | contoso.local\User00899:Jordan 901 | contoso.local\User00900:Mickey 902 | contoso.local\User00901:newpass 903 | contoso.local\User00902: 904 | contoso.local\User00903:foobar 905 | contoso.local\User00904:123456 906 | contoso.local\User00905:123456 907 | contoso.local\User00906: 908 | contoso.local\User00907:12345 909 | contoso.local\User00908:mustang 910 | contoso.local\User00909:Hockey 911 | contoso.local\User00910:Jordan 912 | contoso.local\User00911:12345 913 | contoso.local\User00912:passwd 914 | contoso.local\User00913:Purple 915 | contoso.local\User00914:tigger 916 | contoso.local\User00915:abc123 917 | contoso.local\User00916: 918 | contoso.local\User00917:Beavis 919 | contoso.local\User00918:12345 920 | contoso.local\User00919:orange 921 | contoso.local\User00920:Mickey 922 | contoso.local\User00921:12345 923 | contoso.local\User00922: 924 | contoso.local\User00923:buster 925 | contoso.local\User00924:12345678 926 | contoso.local\User00925:newuser 927 | contoso.local\User00926:newpass 928 | contoso.local\User00927:12345 929 | contoso.local\User00928:12345678 930 | contoso.local\User00929:abc123 931 | contoso.local\User00930:Beavis 932 | contoso.local\User00931:Jordan 933 | contoso.local\User00932: 934 | contoso.local\User00933:jordan 935 | contoso.local\User00934: 936 | contoso.local\User00935: 937 | contoso.local\User00936:password 938 | contoso.local\User00937:12345 939 | contoso.local\User00938:abc123 940 | contoso.local\User00939:passwd 941 | contoso.local\User00940:Jordan 942 | contoso.local\User00941:123456 943 | contoso.local\User00942:password 944 | contoso.local\User00943:notused 945 | contoso.local\User00944:123456 946 | contoso.local\User00945:password 947 | contoso.local\User00946:money 948 | contoso.local\User00947:internet 949 | contoso.local\User00948:joshua 950 | contoso.local\User00949:Michael 951 | contoso.local\User00950:Maddock 952 | contoso.local\User00951:abc123 953 | contoso.local\User00952:Mickey 954 | contoso.local\User00953:newpass 955 | contoso.local\User00954:Cowboys 956 | contoso.local\User00955:123456 957 | contoso.local\User00956:notused 958 | contoso.local\User00957: 959 | contoso.local\User00958:Jordan 960 | contoso.local\User00959: 961 | contoso.local\User00960:Mickey 962 | contoso.local\User00961:Maddock 963 | contoso.local\User00962:ou812 964 | contoso.local\User00963:password 965 | contoso.local\User00964:Mickey 966 | contoso.local\User00965:abc123 967 | contoso.local\User00966:Hockey 968 | contoso.local\User00967:passwd 969 | contoso.local\User00968:michelle 970 | contoso.local\User00969:Smokey 971 | contoso.local\User00970:abc123 972 | contoso.local\User00971:Maddock 973 | contoso.local\User00972:passwd 974 | contoso.local\User00973:Nicole 975 | contoso.local\User00974:Robert 976 | contoso.local\User00975:Maddock 977 | contoso.local\User00976:Cowboys 978 | contoso.local\User00977:Mickey 979 | contoso.local\User00978:passwd 980 | contoso.local\User00979:chris 981 | contoso.local\User00980:money 982 | contoso.local\User00981:passwd 983 | contoso.local\User00982:Hatton 984 | contoso.local\User00983: 985 | contoso.local\User00984:123 986 | contoso.local\User00985:Maddock 987 | contoso.local\User00986:sunshine 988 | contoso.local\User00987:passwd 989 | contoso.local\User00988:newuser 990 | contoso.local\User00989:password 991 | contoso.local\User00990:ou812 992 | contoso.local\User00991:12345678 993 | contoso.local\User00992:Summer 994 | contoso.local\User00993: 995 | contoso.local\User00994:12345 996 | contoso.local\User00995: 997 | contoso.local\User00996:12345 998 | contoso.local\User00997:harley 999 | contoso.local\User00998:Dakota 1000 | contoso.local\User00999:notused 1001 | contoso.local\User01300: 1002 | contoso.local\User01301: 1003 | contoso.local\User01302: 1004 | contoso.local\User01303: 1005 | contoso.local\User01304: 1006 | contoso.local\User01305: 1007 | contoso.local\User01306: 1008 | contoso.local\User01307: 1009 | contoso.local\User01308: 1010 | contoso.local\User01309: 1011 | contoso.local\User01310: 1012 | contoso.local\User01311: 1013 | contoso.local\User01312:user 1014 | contoso.local\User01313:user01313 1015 | contoso.local\User01314:User01314 1016 | -------------------------------------------------------------------------------- /tests/test_db.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the `db` subcommand 3 | """ 4 | 5 | import os 6 | import json 7 | import random 8 | 9 | 10 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 11 | 12 | 13 | def test_submit(config_file, monkeypatch): 14 | from hashcathelper.__main__ import main 15 | from hashcathelper.subcommands import db 16 | from hashcathelper.args import parse_config 17 | from hashcathelper.sql import get_session, Report 18 | 19 | answers = dict( 20 | submitter_email='foo@bar.com', 21 | wordlist='crackstation.txt', 22 | rule_set='OneRule.rule', 23 | hashcat_version='v6.0.1', 24 | ) 25 | 26 | def mock_ask_questions(config): 27 | return answers 28 | 29 | monkeypatch.setattr(db, 'ask_questions', mock_ask_questions) 30 | config = parse_config(config_file) 31 | s = get_session(config.db_uri) 32 | 33 | outfile = os.path.join(SCRIPT_PATH, 'hash.txt.json') 34 | s.query(Report).delete() 35 | 36 | main([ 37 | '--config', 38 | config_file, 39 | 'db', 40 | 'submit', 41 | outfile, 42 | ]) 43 | 44 | r = s.query(Report).one() 45 | 46 | expected_f = os.path.join(SCRIPT_PATH, 'hash.txt.json') 47 | with open(expected_f, 'r') as fp: 48 | expected = json.load(fp) 49 | 50 | print(vars(r)) 51 | assert r.accounts == expected['statistics']['key_quantities']['accounts'] 52 | assert r.submitter_email == answers['submitter_email'] 53 | 54 | # Test queries 55 | main([ 56 | '--config', 57 | config_file, 58 | 'db', 59 | 'stats', 60 | '1', 61 | ]) 62 | 63 | main([ 64 | '--config', 65 | config_file, 66 | 'db', 67 | 'stats', 68 | ]) 69 | 70 | main([ 71 | '--config', 72 | config_file, 73 | 'db', 74 | 'query', 75 | ]) 76 | 77 | 78 | def create_report(accounts, seed=0): 79 | random.seed(seed) 80 | largest_cluster = int(abs(random.gauss(accounts/10, 81 | accounts/50))) 82 | average_password_length = abs(random.gauss(8, 3)) 83 | 84 | result = { 85 | "meta": { 86 | "timestamp": "2021-08-12 11:12:43.706036" 87 | }, 88 | "statistics": { 89 | "key_quantities": { 90 | "removed": 0, 91 | "user_equals_password": [ 92 | 0, 93 | 0.0 94 | ], 95 | "total_accounts": accounts, 96 | "accounts": accounts, 97 | "cluster_count": {}, 98 | "average_password_length": average_password_length, 99 | "median_password_length": 6, 100 | "password_length_count": {}, 101 | "char_class_count": {}, 102 | "average_character_classes": 0 103 | }, 104 | }, 105 | "sensitive_data": { 106 | "top10_passwords": {}, 107 | "top10_basewords": { 108 | "baseword1": largest_cluster, 109 | } 110 | }, 111 | } 112 | 113 | # Fill in values with percentages 114 | N = accounts 115 | for a, parameters in { 116 | 'user_equals_password': (N/100, N/50), 117 | 'lm_hash_count': (N/30, N/50), 118 | 'cracked': (N/3, N/5), 119 | 'nonunique': (N*.3, N*.1), 120 | 'empty_password': (N/100, N/50), 121 | }.items(): 122 | val = min(int(abs(random.gauss(*parameters))), N) 123 | result['statistics']['key_quantities'][a] = [val, 100*val/N] 124 | 125 | return result 126 | 127 | 128 | def test_stats(config_file, capsys): 129 | from hashcathelper.__main__ import main 130 | from hashcathelper.args import parse_config 131 | from hashcathelper.sql import get_session, Report, submit 132 | from hashcathelper.analytics import create_short_report 133 | from hashcathelper.subcommands.db import get_stats 134 | 135 | config = parse_config(config_file) 136 | s = get_session(config.db_uri) 137 | s.query(Report).delete() 138 | 139 | random.seed(0) 140 | 141 | for i in range(100): 142 | data = create_report(random.randint(200, 200000), seed=i) 143 | short_report = create_short_report( 144 | 'foo', 'wordlist', 'rule', '0.0', data 145 | ) 146 | submit(s, short_report) 147 | 148 | main([ 149 | '--config', 150 | config_file, 151 | 'db', 152 | 'stats', 153 | ]) 154 | capture = capsys.readouterr() 155 | print(capture.out) 156 | assert capture.out 157 | 158 | main([ 159 | '--config', 160 | config_file, 161 | 'db', 162 | 'stats', 163 | '--format', 'json', 164 | ]) 165 | capture = capsys.readouterr() 166 | print(capture.out) 167 | assert capture.out 168 | 169 | expected = { 170 | 1: {'cracked': [13.0, 35.72, 18.48, 86], 'nonunique': 171 | [29.27, 29.22, 10.55, 54], 'user_equals_password': 172 | [0.35, 1.89, 1.41, 91], 'lm_hash_count': [4.07, 3.42, 173 | 1.84, 33], 174 | 'empty_password': [1.35, 1.63, 1.25, 50], 175 | 'largest_baseword_cluster': [11.88, 9.78, 1.93, 15], 176 | 'average_password_length': [3.81, 8.58, 2.93, 6]}, 177 | 10: {'cracked': [56.95, 35.72, 18.48, 9], 'nonunique': 178 | [30.47, 29.22, 10.55, 43], 179 | 'user_equals_password': [3.58, 1.89, 1.41, 9], 'lm_hash_count': 180 | [6.4, 3.42, 1.84, 6], 'empty_password': [1.65, 1.63, 1.25, 43], 181 | 'largest_baseword_cluster': [8.11, 9.78, 1.93, 77], 182 | 'average_password_length': [8.66, 8.58, 2.93, 47]}, 183 | 20: {'cracked': [19.87, 35.72, 18.48, 73], 'nonunique': 184 | [51.03, 29.22, 10.55, 1], 185 | 'user_equals_password': [1.37, 1.89, 1.41, 60], 'lm_hash_count': 186 | [3.02, 3.42, 1.84, 53], 'empty_password': [0.72, 1.63, 1.25, 70], 187 | 'largest_baseword_cluster': [8.44, 9.78, 1.93, 71], 188 | 'average_password_length': [3.28, 8.58, 2.93, 4]}, 189 | } 190 | 191 | s = get_session(config.db_uri) 192 | for i, val in expected.items(): 193 | r = s.query(Report).filter_by(id=i).one() 194 | all_entries = s.query(Report).all() 195 | result = get_stats(r, all_entries) 196 | print(result) 197 | assert result == val 198 | -------------------------------------------------------------------------------- /tests/test_ntlm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the `ntlm` subcommand 3 | """ 4 | 5 | import os 6 | 7 | import pytest 8 | 9 | 10 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 11 | 12 | 13 | def get_lmhash(password): 14 | from Crypto.Cipher import DES 15 | import binascii 16 | 17 | if password is None: 18 | return bytes.fromhex('aad3b435b51404eeaad3b435b51404ee') 19 | 20 | LM_SECRET = b'KGS!@#$%' 21 | password_uppercase = password.upper() 22 | password_uppercase_bytes = password_uppercase.encode('ascii') 23 | password_uppercase_bytes_padded = \ 24 | password_uppercase_bytes.ljust(14, b'\x00') 25 | password_chunk_1 = password_uppercase_bytes_padded[0:7] 26 | password_chunk_2 = password_uppercase_bytes_padded[7:] 27 | des_chunk_1 = DES.new(expand_DES_key(password_chunk_1), DES.MODE_ECB) 28 | des_chunk_2 = DES.new(expand_DES_key(password_chunk_2), DES.MODE_ECB) 29 | des_first_half = des_chunk_1.encrypt(LM_SECRET) 30 | des_second_half = des_chunk_2.encrypt(LM_SECRET) 31 | lm_hash = des_first_half + des_second_half 32 | result = binascii.hexlify(lm_hash) 33 | return result 34 | 35 | 36 | # from impacket 37 | def expand_DES_key(key): 38 | # Expand the key from a 7-byte password key into a 8-byte DES key 39 | key = key[:7] 40 | key += b'\x00'*(7-len(key)) 41 | s = [ 42 | (((key[0] >> 1) & 0x7f) << 1), 43 | (((key[0] & 0x01) << 6 | ((key[1] >> 2) & 0x3f)) << 1), 44 | (((key[1] & 0x03) << 5 | ((key[2] >> 3) & 0x1f)) << 1), 45 | (((key[2] & 0x07) << 4 | ((key[3] >> 4) & 0x0f)) << 1), 46 | (((key[3] & 0x0f) << 3 | ((key[4] >> 5) & 0x07)) << 1), 47 | (((key[4] & 0x1f) << 2 | ((key[5] >> 6) & 0x03)) << 1), 48 | (((key[5] & 0x3f) << 1 | ((key[6] >> 7) & 0x01)) << 1), 49 | ((key[6] & 0x7f) << 1), 50 | ] 51 | return b''.join([x.to_bytes(1, byteorder='big') for x in s]) 52 | 53 | 54 | @pytest.fixture(scope='session') 55 | def words(): 56 | with open(os.path.join(SCRIPT_PATH, 'words'), 'r') as fp: 57 | words = fp.read() 58 | yield words.splitlines() 59 | 60 | 61 | @pytest.fixture(scope='session') 62 | def temp_dir(): 63 | import tempfile 64 | yield tempfile.mkdtemp(prefix="hch_tempdir") 65 | 66 | 67 | def create_pwdump(WORDS, 68 | seed=0, 69 | password_picks=20, 70 | random_passwords=0, 71 | user_eq_pass=0, 72 | empty=0, 73 | use_lm_hash=0, 74 | domain='contoso.local', 75 | weight_power=0): 76 | from hashcathelper.utils import get_nthash 77 | from hashcathelper.consts import LM_EMPTY 78 | import random 79 | import string 80 | 81 | random.seed(seed) 82 | passwords = [] 83 | 84 | if password_picks: 85 | passwords += random.choices( 86 | WORDS, 87 | k=password_picks, 88 | weights=[1/(i/10+1)**weight_power for i, _ in enumerate(WORDS)], 89 | ) 90 | if random_passwords: 91 | for _ in range(random_passwords): 92 | passwords.append(''.join(random.choices(string.ascii_uppercase, 93 | k=16))) 94 | if empty: 95 | passwords += ['']*empty 96 | 97 | pw_dump = [] 98 | cleartext = [] 99 | 100 | for i, pw in enumerate(passwords): 101 | if i < use_lm_hash: 102 | lm_hash = get_lmhash(pw).decode() 103 | else: 104 | lm_hash = LM_EMPTY 105 | nt_hash = get_nthash(pw.encode()) 106 | username = '%s\\User%05d' % (domain, i) 107 | pw_dump.append( 108 | '%s:%d:%s:%s:::' % ( 109 | username, i, lm_hash, nt_hash 110 | ) 111 | ) 112 | cleartext.append('%s:%s' % (username, pw)) 113 | for i in range(user_eq_pass): 114 | user = "User%05d" % len(pw_dump) 115 | username = "%s\\%s" % (domain, user) 116 | nt_hash = get_nthash(user.encode()) 117 | pw_dump.append( 118 | '%s:%d:%s:%s:::' % ( 119 | username, i, LM_EMPTY, nt_hash 120 | ) 121 | ) 122 | cleartext.append('%s:%s' % (username, user)) 123 | 124 | return pw_dump, cleartext 125 | 126 | 127 | def test_ntlm(temp_dir, words, config_file): 128 | import os 129 | 130 | from hashcathelper.__main__ import main 131 | 132 | random_passwords = 300 133 | pw_dump, cleartext = create_pwdump( 134 | words, 135 | password_picks=1000, 136 | random_passwords=random_passwords, 137 | user_eq_pass=20, 138 | empty=15, 139 | use_lm_hash=17, 140 | weight_power=2, 141 | ) 142 | 143 | tmp_hash = os.path.join(temp_dir, 'hash.txt') 144 | with open(tmp_hash, 'w') as fp: 145 | fp.write('\n'.join(pw_dump)+'\n') 146 | 147 | main([ 148 | '--config', 149 | config_file, 150 | 'ntlm', 151 | '--skip-lm', 152 | tmp_hash, 153 | ]) 154 | 155 | with open(tmp_hash + '.out', 'r') as fp: 156 | cracked_count = 0 157 | for line in fp.readlines(): 158 | cracked_count += 1 159 | assert line[:-1] in cleartext 160 | 161 | assert cracked_count + random_passwords == len(cleartext) 162 | 163 | 164 | def test_report(): 165 | import json 166 | from hashcathelper.analytics import create_report 167 | 168 | hashfile = os.path.join(SCRIPT_PATH, 'hash.txt') 169 | outfile = os.path.join(SCRIPT_PATH, 'hash.txt.out') 170 | expected_f = os.path.join(SCRIPT_PATH, 'hash.txt.json') 171 | with open(expected_f, 'r') as fp: 172 | expected = json.load(fp) 173 | 174 | report = create_report(hashfile, outfile, degree_of_detail=3) 175 | report_json = report.json() 176 | del report_json['meta'] 177 | print(json.dumps(report_json, indent=2)) 178 | assert report_json == expected 179 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the `utils` module 3 | """ 4 | 5 | import os 6 | 7 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 8 | 9 | 10 | def compare(user, dct): 11 | if 'disabled' in dct: 12 | assert dct['disabled'] == user.is_disabled() 13 | del dct['disabled'] 14 | if 'computer_account' in dct: 15 | assert dct['computer_account'] == user.is_computer_account() 16 | del dct['computer_acccount'] 17 | for k, v in dct.items(): 18 | assert getattr(user, k) == v 19 | 20 | 21 | def test_user(): 22 | from hashcathelper.utils import User 23 | test_cases = { 24 | 'group.local\\Administrator:': dict( 25 | username='Administrator', 26 | upn_suffix='group.local', 27 | password='', 28 | disabled=False, 29 | ), 30 | 31 | r'contoso.local\User01313:1313:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0::: (status=Disabled)': dict( # noqa 32 | username='User01313', 33 | upn_suffix='contoso.local', 34 | password=None, 35 | comment='(status=Disabled)', 36 | disabled=True, 37 | ), 38 | 'user:$HEX[68616c6c6f32303039f0f0]': dict( 39 | username='user', 40 | password='hallo2009', 41 | ), 42 | } 43 | for line, expected in test_cases.items(): 44 | u = User(line) 45 | print(u) 46 | compare(u, expected) 47 | -------------------------------------------------------------------------------- /tests/words: -------------------------------------------------------------------------------- 1 | 2 | 12345 3 | abc123 4 | password 5 | passwd 6 | 123456 7 | newpass 8 | notused 9 | Hockey 10 | internet 11 | Maddock 12 | 12345678 13 | newuser 14 | computer 15 | Internet 16 | Mickey 17 | qwerty 18 | fiction 19 | Cowboys 20 | Jordan 21 | Hatton 22 | test 23 | Michael 24 | ou812 25 | orange 26 | 1234 27 | Beavis 28 | 123 29 | tigger 30 | Soccer 31 | shadow 32 | Purple 33 | Sports 34 | dragon 35 | michael 36 | wheeling 37 | mustang 38 | Monkey 39 | Qwerty 40 | School 41 | Snoopy 42 | Vikings 43 | jennifer 44 | money 45 | Justin 46 | mickey 47 | 0246 48 | a1b2c3 49 | chris 50 | david 51 | foobar 52 | Robert 53 | buster 54 | harley 55 | jordan 56 | stupid 57 | clever 58 | apple 59 | fred 60 | 123abc 61 | Amanda 62 | Dakota 63 | summer 64 | sunshine 65 | andrew 66 | hello 67 | maggie 68 | monday 69 | pascal 70 | patrick 71 | Dallas 72 | Jessica 73 | Nicole 74 | Sendit 75 | Smokey 76 | baseball 77 | daniel 78 | diamond 79 | joshua 80 | michelle 81 | mike 82 | silver 83 | 1q2w3e 84 | Friends 85 | George 86 | Shadow 87 | Summer 88 | bandit 89 | coffee 90 | falcon 91 | pepper 92 | richard 93 | thomas 94 | undead 95 | !@#$% 96 | Andrew 97 | Buster 98 | Cowboy 99 | Eagles 100 | --------------------------------------------------------------------------------