Merge pull request #59 from trustedsec/rules

automatic rules
This commit is contained in:
Justin Bollinger
2026-01-26 22:58:09 -05:00
committed by GitHub
4 changed files with 348 additions and 59 deletions
+9
View File
@@ -33,6 +33,15 @@ Core logic is now split into modules under `hate_crack/`:
The top-level `hate_crack.py` remains the main entry point and orchestrates these modules. The top-level `hate_crack.py` remains the main entry point and orchestrates these modules.
-------------------------------------------------------------------
## References and Thanks
This project depends on and is inspired by a number of external projects and services. Thanks to:
- Hashview (http://github.com/hashview/)
- Weakpass (https://weakpass.com)
- Hashmob (https://hashmob.net)
------------------------------------------------------------------- -------------------------------------------------------------------
## Usage ## Usage
`$ ./hate_crack.py` `$ ./hate_crack.py`
+2 -1
View File
@@ -4,6 +4,7 @@
"hcatTuning": "--force --remove", "hcatTuning": "--force --remove",
"hcatWordlists": "/Passwords/wordlists", "hcatWordlists": "/Passwords/wordlists",
"hcatOptimizedWordlists": "/Passwords/optimized_wordlists", "hcatOptimizedWordlists": "/Passwords/optimized_wordlists",
"rules_directory": "/path/to/hashcat/rules",
"hcatDictionaryWordlist": ["rockyou.txt"], "hcatDictionaryWordlist": ["rockyou.txt"],
"hcatCombinationWordlist": ["rockyou.txt","rockyou.txt"], "hcatCombinationWordlist": ["rockyou.txt","rockyou.txt"],
"hcatHybridlist": ["rockyou.txt"], "hcatHybridlist": ["rockyou.txt"],
@@ -19,4 +20,4 @@
"bandrel_common_basedwords": "welcome,password,p@ssword,p@$$word,changeme,letmein,summer,winter,spring,springtime,fall,autumn,monday,tuesday,wednesday,thursday,friday,saturday,sunday,january,february,march,april,may,june,july,august,september,october,november,december,christmas,easter,covid19", "bandrel_common_basedwords": "welcome,password,p@ssword,p@$$word,changeme,letmein,summer,winter,spring,springtime,fall,autumn,monday,tuesday,wednesday,thursday,friday,saturday,sunday,january,february,march,april,may,june,july,august,september,october,november,december,christmas,easter,covid19",
"hashview_url": "http://localhost:8443", "hashview_url": "http://localhost:8443",
"hashview_api_key": "" "hashview_api_key": ""
} }
+87 -19
View File
@@ -104,6 +104,7 @@ from hate_crack.api import (
download_all_weakpass_torrents, download_all_weakpass_torrents,
download_hashes_from_hashview, download_hashes_from_hashview,
download_hashmob_wordlists, download_hashmob_wordlists,
download_hashmob_rules,
download_weakpass_torrent, download_weakpass_torrent,
extract_with_7z, extract_with_7z,
) )
@@ -120,6 +121,17 @@ hcatWordlists = config_parser['hcatWordlists']
hcatOptimizedWordlists = config_parser['hcatOptimizedWordlists'] hcatOptimizedWordlists = config_parser['hcatOptimizedWordlists']
hcatRules = [] hcatRules = []
try:
rulesDirectory = config_parser['rules_directory']
except KeyError as e:
print('{0} is not defined in config.json using defaults from config.json.example'.format(e))
rulesDirectory = default_config.get('rules_directory')
if not rulesDirectory:
rulesDirectory = os.path.join(hcatPath, "rules") if hcatPath else os.path.join(hate_path, "rules")
rulesDirectory = os.path.expanduser(rulesDirectory)
if not os.path.isabs(rulesDirectory):
rulesDirectory = os.path.join(hate_path, rulesDirectory)
try: try:
maxruntime = config_parser['bandrelmaxruntime'] maxruntime = config_parser['bandrelmaxruntime']
except KeyError as e: except KeyError as e:
@@ -219,6 +231,38 @@ def verify_wordlist_dir(directory, wordlist):
print('Exiting. Please check configuration and try again.') print('Exiting. Please check configuration and try again.')
return None return None
def get_rule_path(rule_name, fallback_dir=None):
candidates = []
if rulesDirectory:
candidates.append(os.path.join(rulesDirectory, rule_name))
if fallback_dir:
candidates.append(os.path.join(fallback_dir, rule_name))
for candidate in candidates:
if os.path.isfile(candidate):
return candidate
return candidates[0] if candidates else rule_name
def ensure_toggle_rule():
"""Ensure toggles-lm-ntlm.rule exists in the configured rules directory."""
if not rulesDirectory:
return None
target_path = os.path.join(rulesDirectory, "toggles-lm-ntlm.rule")
if os.path.isfile(target_path):
return target_path
source_path = os.path.join(hate_path, "rules", "toggles-lm-ntlm.rule")
try:
os.makedirs(rulesDirectory, exist_ok=True)
if os.path.isfile(source_path):
with open(source_path, "r") as src, open(target_path, "w") as dst:
dst.write(src.read())
else:
with open(target_path, "w") as dst:
dst.write("l\nu\n")
print(f"[i] Created rule file: {target_path}")
except Exception as e:
print(f"[!] Failed to create toggles-lm-ntlm.rule: {e}")
return target_path
def cleanup_wordlist_artifacts(): def cleanup_wordlist_artifacts():
wordlists_dir = hcatWordlists or os.path.join(hate_path, "wordlists") wordlists_dir = hcatWordlists or os.path.join(hate_path, "wordlists")
if not os.path.isabs(wordlists_dir): if not os.path.isabs(wordlists_dir):
@@ -479,15 +523,16 @@ def hcatBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
def hcatDictionary(hcatHashType, hcatHashFile): def hcatDictionary(hcatHashType, hcatHashFile):
global hcatDictionaryCount global hcatDictionaryCount
global hcatProcess global hcatProcess
rule_best66 = get_rule_path("best66.rule")
hcatProcess = subprocess.Popen( hcatProcess = subprocess.Popen(
"{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {optimized_wordlists}/* " "{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {optimized_wordlists}/* "
"-r {hcatPath}/rules/best66.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( "-r {rule_best66} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatPath=hcatPath,
hcatBin=hcatBin, hcatBin=hcatBin,
hcatHashType=hcatHashType, hcatHashType=hcatHashType,
hash_file=hcatHashFile, hash_file=hcatHashFile,
session_name=generate_session_id(), session_name=generate_session_id(),
optimized_wordlists=hcatOptimizedWordlists, optimized_wordlists=hcatOptimizedWordlists,
rule_best66=rule_best66,
tuning=hcatTuning, tuning=hcatTuning,
hate_path=hate_path), shell=True) hate_path=hate_path), shell=True)
try: try:
@@ -498,15 +543,16 @@ def hcatDictionary(hcatHashType, hcatHashFile):
for wordlist in hcatDictionaryWordlist: for wordlist in hcatDictionaryWordlist:
rule_d3ad0ne = get_rule_path("d3ad0ne.rule")
hcatProcess = subprocess.Popen( hcatProcess = subprocess.Popen(
"{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {hcatWordlist} " "{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {hcatWordlist} "
"-r {hcatPath}/rules/d3ad0ne.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( "-r {rule_d3ad0ne} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatPath=hcatPath,
hcatBin=hcatBin, hcatBin=hcatBin,
hcatHashType=hcatHashType, hcatHashType=hcatHashType,
hash_file=hcatHashFile, hash_file=hcatHashFile,
session_name=generate_session_id(), session_name=generate_session_id(),
hcatWordlist=wordlist, hcatWordlist=wordlist,
rule_d3ad0ne=rule_d3ad0ne,
tuning=hcatTuning, tuning=hcatTuning,
hate_path=hate_path), shell=True) hate_path=hate_path), shell=True)
try: try:
@@ -515,16 +561,16 @@ def hcatDictionary(hcatHashType, hcatHashFile):
print('Killing PID {0}...'.format(str(hcatProcess.pid))) print('Killing PID {0}...'.format(str(hcatProcess.pid)))
hcatProcess.kill() hcatProcess.kill()
rule_toxic = get_rule_path("T0XlC.rule")
hcatProcess = subprocess.Popen( hcatProcess = subprocess.Popen(
"{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {hcatWordlist} " "{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {hcatWordlist} "
"-r {hcatPath}/rules/T0XlC.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( "-r {rule_toxic} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatPath=hcatPath,
hcatBin=hcatBin, hcatBin=hcatBin,
hcatHashType=hcatHashType, hcatHashType=hcatHashType,
hash_file=hcatHashFile, hash_file=hcatHashFile,
session_name=generate_session_id(), session_name=generate_session_id(),
hcatWordlist=wordlist, hcatWordlist=wordlist,
rule_toxic=rule_toxic,
tuning=hcatTuning, tuning=hcatTuning,
hate_path=hate_path), shell=True) hate_path=hate_path), shell=True)
try: try:
@@ -1037,16 +1083,19 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile):
def hcatPrince(hcatHashType, hcatHashFile): def hcatPrince(hcatHashType, hcatHashFile):
global hcatProcess global hcatProcess
hcatHashCracked = lineCount(hcatHashFile + ".out") hcatHashCracked = lineCount(hcatHashFile + ".out")
prince_rules_dir = os.path.join(hate_path, "princeprocessor", "rules")
prince_rule = get_rule_path("prince_optimized.rule", fallback_dir=prince_rules_dir)
hcatProcess = subprocess.Popen( hcatProcess = subprocess.Popen(
"{hate_path}/princeprocessor/{prince_bin} --case-permute --elem-cnt-min=1 --elem-cnt-max=16 -c < " "{hate_path}/princeprocessor/{prince_bin} --case-permute --elem-cnt-min=1 --elem-cnt-max=16 -c < "
"{hcatPrinceBaseList} | {hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out " "{hcatPrinceBaseList} | {hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out "
"-r {hate_path}/princeprocessor/rules/prince_optimized.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( "-r {prince_rule} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatBin=hcatBin, hcatBin=hcatBin,
prince_bin=hcatPrinceBin, prince_bin=hcatPrinceBin,
hash_type=hcatHashType, hash_type=hcatHashType,
hash_file=hcatHashFile, hash_file=hcatHashFile,
session_name=generate_session_id(), session_name=generate_session_id(),
hcatPrinceBaseList=hcatPrinceBaseList, hcatPrinceBaseList=hcatPrinceBaseList,
prince_rule=prince_rule,
tuning=hcatTuning, tuning=hcatTuning,
hate_path=hate_path), shell=True) hate_path=hate_path), shell=True)
try: try:
@@ -1059,16 +1108,18 @@ def hcatPrince(hcatHashType, hcatHashFile):
def hcatGoodMeasure(hcatHashType, hcatHashFile): def hcatGoodMeasure(hcatHashType, hcatHashFile):
global hcatExtraCount global hcatExtraCount
global hcatProcess global hcatProcess
rule_combinator = get_rule_path("combinator.rule")
rule_insidepro = get_rule_path("InsidePro-PasswordsPro.rule")
hcatProcess = subprocess.Popen( hcatProcess = subprocess.Popen(
"{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -r {hcatPath}/rules/combinator.rule " "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -r {rule_combinator} "
"-r {hcatPath}/rules/InsidePro-PasswordsPro.rule {hcatGoodMeasureBaseList} {tuning} " "-r {rule_insidepro} {hcatGoodMeasureBaseList} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
"--potfile-path={hate_path}/hashcat.pot".format(
hcatPath=hcatPath,
hcatBin=hcatBin, hcatBin=hcatBin,
hash_type=hcatHashType, hash_type=hcatHashType,
hash_file=hcatHashFile, hash_file=hcatHashFile,
hcatGoodMeasureBaseList=hcatGoodMeasureBaseList, hcatGoodMeasureBaseList=hcatGoodMeasureBaseList,
session_name=generate_session_id(), session_name=generate_session_id(),
rule_combinator=rule_combinator,
rule_insidepro=rule_insidepro,
tuning=hcatTuning, tuning=hcatTuning,
hate_path=hate_path), shell=True) hate_path=hate_path), shell=True)
try: try:
@@ -1136,10 +1187,11 @@ def hcatLMtoNT():
hcatProcess = subprocess.Popen( hcatProcess = subprocess.Popen(
"{hcatBin} -m 1000 {hash_file}.nt --session {session_name} -o {hash_file}.nt.out {hash_file}.combined " "{hcatBin} -m 1000 {hash_file}.nt --session {session_name} -o {hash_file}.nt.out {hash_file}.combined "
"-r {hate_path}/rules/toggles-lm-ntlm.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( "-r {toggle_rule} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatBin=hcatBin, hcatBin=hcatBin,
hash_file=hcatHashFile, hash_file=hcatHashFile,
session_name=generate_session_id(), session_name=generate_session_id(),
toggle_rule=ensure_toggle_rule() or get_rule_path("toggles-lm-ntlm.rule", fallback_dir=os.path.join(hate_path, "rules")),
tuning=hcatTuning, tuning=hcatTuning,
hate_path=hate_path), shell=True) hate_path=hate_path), shell=True)
try: try:
@@ -1170,15 +1222,15 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords):
with open(working_file, 'w') as f: with open(working_file, 'w') as f:
f.write("\n".join(converted)) f.write("\n".join(converted))
for rule in hcatRules: for rule in hcatRules:
rule_path = get_rule_path(rule)
hcatProcess = subprocess.Popen( hcatProcess = subprocess.Popen(
"{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out {hash_file}.working " "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out {hash_file}.working "
"-r {hcatPath}/rules/{rule} {tuning} --potfile-path={hate_path}/hashcat.pot".format( "-r {rule_path} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
rule=rule,
hcatBin=hcatBin, hcatBin=hcatBin,
hash_type=hcatHashType, hash_type=hcatHashType,
hash_file=hcatHashFile, hash_file=hcatHashFile,
session_name=generate_session_id(), session_name=generate_session_id(),
hcatPath=hcatPath, rule_path=rule_path,
tuning=hcatTuning, tuning=hcatTuning,
hate_path=hate_path), shell=True) hate_path=hate_path), shell=True)
try: try:
@@ -1698,7 +1750,11 @@ def pipal():
print('Killing PID {0}...'.format(str(pipalProcess.pid))) print('Killing PID {0}...'.format(str(pipalProcess.pid)))
pipalProcess.kill() pipalProcess.kill()
print("Pipal file is at " + hcatHashFilePipal + ".pipal\n") print("Pipal file is at " + hcatHashFilePipal + ".pipal\n")
view_choice = input("Would you like to view (cat) the pipal output? (Y/n): ").strip().lower() import sys
if not sys.stdin.isatty():
view_choice = 'y'
else:
view_choice = input("Would you like to view (cat) the pipal output? (Y/n): ").strip().lower()
if view_choice in ('', 'y', 'yes'): if view_choice in ('', 'y', 'yes'):
print("\n--- Pipal Output Start ---\n") print("\n--- Pipal Output Start ---\n")
with open(hcatHashFilePipal + ".pipal") as pipalfile: with open(hcatHashFilePipal + ".pipal") as pipalfile:
@@ -1802,7 +1858,7 @@ def main():
global lmHashesFound global lmHashesFound
global debug_mode global debug_mode
global hashview_url, hashview_api_key global hashview_url, hashview_api_key
global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists, rulesDirectory
global pipalPath, maxruntime, bandrelbasewords global pipalPath, maxruntime, bandrelbasewords
@@ -1818,6 +1874,7 @@ def main():
parser.add_argument('--weakpass', action='store_true', help='Download wordlists from Weakpass') parser.add_argument('--weakpass', action='store_true', help='Download wordlists from Weakpass')
parser.add_argument('--rank', type=int, default=-1, help='Only show wordlists with this rank (use 0 to show all, default: >4)') parser.add_argument('--rank', type=int, default=-1, help='Only show wordlists with this rank (use 0 to show all, default: >4)')
parser.add_argument('--hashmob', action='store_true', help='Download wordlists from Hashmob.net') parser.add_argument('--hashmob', action='store_true', help='Download wordlists from Hashmob.net')
parser.add_argument('--rules', action='store_true', help='Download rules from Hashmob.net')
parser.add_argument('--cleanup', action='store_true', help='Cleanup .out files, torrents, and extract or remove .7z archives') parser.add_argument('--cleanup', action='store_true', help='Cleanup .out files, torrents, and extract or remove .7z archives')
parser.add_argument('--debug', action='store_true', help='Enable debug mode') parser.add_argument('--debug', action='store_true', help='Enable debug mode')
# Removed add_common_args(parser) since config items are now only set via config file # Removed add_common_args(parser) since config items are now only set via config file
@@ -1837,6 +1894,7 @@ def main():
hcatBin=hcatBin, hcatBin=hcatBin,
hcatWordlists=hcatWordlists, hcatWordlists=hcatWordlists,
hcatOptimizedWordlists=hcatOptimizedWordlists, hcatOptimizedWordlists=hcatOptimizedWordlists,
rules_directory=rulesDirectory,
pipalPath=pipalPath, pipalPath=pipalPath,
maxruntime=maxruntime, maxruntime=maxruntime,
bandrelbasewords=bandrelbasewords, bandrelbasewords=bandrelbasewords,
@@ -1848,6 +1906,7 @@ def main():
hcatBin = config.hcatBin hcatBin = config.hcatBin
hcatWordlists = config.hcatWordlists hcatWordlists = config.hcatWordlists
hcatOptimizedWordlists = config.hcatOptimizedWordlists hcatOptimizedWordlists = config.hcatOptimizedWordlists
rulesDirectory = config.rules_directory
pipalPath = config.pipalPath pipalPath = config.pipalPath
maxruntime = config.maxruntime maxruntime = config.maxruntime
bandrelbasewords = config.bandrelbasewords bandrelbasewords = config.bandrelbasewords
@@ -1905,6 +1964,9 @@ def main():
if args.hashmob: if args.hashmob:
download_hashmob_wordlists(print_fn=print) download_hashmob_wordlists(print_fn=print)
sys.exit(0) sys.exit(0)
if args.rules:
download_hashmob_rules(print_fn=print)
sys.exit(0)
if args.hashfile and args.hashtype: if args.hashfile and args.hashtype:
hcatHashFile = resolve_path(args.hashfile) hcatHashFile = resolve_path(args.hashfile)
@@ -1923,7 +1985,8 @@ def main():
print("\t(1) Download hashes from Hashview") print("\t(1) Download hashes from Hashview")
print("\t(2) Download wordlists from Weakpass") print("\t(2) Download wordlists from Weakpass")
print("\t(3) Download wordlists from Hashmob.net") print("\t(3) Download wordlists from Hashmob.net")
print("\t(4) Exit") print("\t(4) Download rules from Hashmob.net")
print("\t(5) Exit")
choice = input("\nSelect an option: ") choice = input("\nSelect an option: ")
if choice == '1' or args.download_hashview: if choice == '1' or args.download_hashview:
if not hashview_api_key: if not hashview_api_key:
@@ -1950,6 +2013,9 @@ def main():
elif choice == '3' or args.hashmob: elif choice == '3' or args.hashmob:
download_hashmob_wordlists(print_fn=print) download_hashmob_wordlists(print_fn=print)
sys.exit(0) sys.exit(0)
elif choice == '4' or args.rules:
download_hashmob_rules(print_fn=print)
sys.exit(0)
else: else:
sys.exit(0) sys.exit(0)
@@ -2031,6 +2097,7 @@ def main():
"11": middle_combinator, "11": middle_combinator,
"12": thorough_combinator, "12": thorough_combinator,
"13": bandrel_method, "13": bandrel_method,
"91": download_hashmob_rules,
"92": weakpass_wordlist_menu, "92": weakpass_wordlist_menu,
"93": download_hashmob_wordlists, "93": download_hashmob_wordlists,
"94": weakpass_wordlist_menu, "94": weakpass_wordlist_menu,
@@ -2055,6 +2122,7 @@ def main():
print("\t(11) Middle Combinator Attack") print("\t(11) Middle Combinator Attack")
print("\t(12) Thorough Combinator Attack") print("\t(12) Thorough Combinator Attack")
print("\t(13) Bandrel Methodology") print("\t(13) Bandrel Methodology")
print("\n\t(91) Download rules from Hashmob.net")
print("\n\t(92) Download wordlists from Weakpass") print("\n\t(92) Download wordlists from Weakpass")
print("\t(93) Download wordlists from Hashmob.net") print("\t(93) Download wordlists from Hashmob.net")
print("\t(94) Weakpass Wordlist Menu") print("\t(94) Weakpass Wordlist Menu")
+250 -39
View File
@@ -57,6 +57,31 @@ def get_hcat_wordlists_dir():
os.makedirs(default, exist_ok=True) os.makedirs(default, exist_ok=True)
return default return default
def get_rules_dir():
pkg_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(pkg_dir, os.pardir))
candidates = [
os.path.join(pkg_dir, 'config.json'),
os.path.join(project_root, 'config.json')
]
default = os.path.join(project_root, 'rules')
for config_path in candidates:
try:
if os.path.isfile(config_path):
with open(config_path) as f:
config = json.load(f)
path = config.get('rules_directory')
if path:
path = os.path.expanduser(path)
if not os.path.isabs(path):
path = os.path.join(project_root, path)
os.makedirs(path, exist_ok=True)
return path
except Exception:
continue
os.makedirs(default, exist_ok=True)
return default
def cleanup_torrent_files(directory=None): def cleanup_torrent_files(directory=None):
"""Remove stray .torrent files from the wordlists directory on graceful exit.""" """Remove stray .torrent files from the wordlists directory on graceful exit."""
if directory is None: if directory is None:
@@ -165,9 +190,27 @@ def download_torrent_file(torrent_url, save_dir=None, wordlist_id=None):
if not os.path.isabs(save_dir): if not os.path.isabs(save_dir):
save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), save_dir) save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), save_dir)
os.makedirs(save_dir, exist_ok=True) os.makedirs(save_dir, exist_ok=True)
# Optionally include hashmob_api_key in headers if present
headers = { headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
} }
hashmob_api_key = None
# Try to get hashmob_api_key from config
pkg_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(pkg_dir, os.pardir))
for cfg in (os.path.join(pkg_dir, 'config.json'), os.path.join(project_root, 'config.json')):
if os.path.isfile(cfg):
try:
with open(cfg) as f:
config = json.load(f)
key = config.get('hashmob_api_key')
if key:
hashmob_api_key = key
break
except Exception:
continue
if hashmob_api_key:
headers["api-key"] = hashmob_api_key
# Resolve a filename even if a URL is provided. # Resolve a filename even if a URL is provided.
if not torrent_url.startswith("http"): if not torrent_url.startswith("http"):
@@ -759,47 +802,136 @@ def download_hashmob_wordlist(file_name, out_path):
url = f"https://hashmob.net/api/v2/downloads/research/wordlists/{file_name}" url = f"https://hashmob.net/api/v2/downloads/research/wordlists/{file_name}"
api_key = get_hashmob_api_key() api_key = get_hashmob_api_key()
headers = {"api-key": api_key} if api_key else {} headers = {"api-key": api_key} if api_key else {}
import time
base_backoff = 256
max_backoff = 300
penalty_add = 2
penalty = base_backoff
import threading
lock = getattr(download_hashmob_wordlist, '_rate_lock', None)
if lock is None:
lock = threading.Lock()
download_hashmob_wordlist._rate_lock = lock
while True:
with lock:
time.sleep(15)
try:
with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r:
if r.status_code == 429:
print(f"[!] Rate limit hit (429). Backing off for {penalty} seconds...")
time.sleep(penalty)
penalty = min(penalty + penalty_add, max_backoff)
penalty_add *= 2
continue
if r.status_code in (301, 302, 303, 307, 308):
redirect_url = r.headers.get('Location')
if redirect_url:
print(f"Following redirect to: {redirect_url}")
return download_hashmob_wordlist(redirect_url, out_path)
print("Redirect with no Location header!")
return False
r.raise_for_status()
content_type = r.headers.get('Content-Type', '')
if 'text/plain' in content_type:
html = r.content.decode(errors='replace')
import re
match = re.search(
r"<meta[^>]+http-equiv=['\"]refresh['\"][^>]+content=['\"]0;url=([^'\"]+)['\"]",
html,
re.IGNORECASE
)
if match:
real_url = match.group(1)
print(f"Found meta refresh redirect to: {real_url}")
with requests.get(real_url, stream=True, timeout=120) as r2:
r2.raise_for_status()
with open(out_path, 'wb') as f:
for chunk in r2.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Downloaded {out_path}")
return True
print("Error: Received HTML instead of file. Possible permission or quota issue.")
return False
with open(out_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Downloaded {out_path}")
return True
except Exception as e:
if hasattr(e, 'response') and getattr(e.response, 'status_code', None) == 429:
print(f"[!] Rate limit hit (429). Backing off for {penalty} seconds...")
time.sleep(penalty)
penalty = min(penalty + penalty_add, max_backoff)
penalty_add *= 2
continue
print(f"Error downloading wordlist: {e}")
return False
def download_hashmob_rule_list():
"""Fetch available rules from Hashmob API v2 and print them."""
url = "https://hashmob.net/api/v2/resource"
api_key = get_hashmob_api_key()
headers = {"api-key": api_key} if api_key else {}
try: try:
with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r: resp = requests.get(url, headers=headers, timeout=30)
if r.status_code in (301, 302, 303, 307, 308): resp.raise_for_status()
redirect_url = r.headers.get('Location') data = resp.json()
if redirect_url: rules = [r for r in data if r.get('type') in ('rule', 'official_rule')]
print(f"Following redirect to: {redirect_url}") print("Available Hashmob Rules:")
return download_hashmob_wordlist(redirect_url, out_path) for idx, rule in enumerate(rules):
print("Redirect with no Location header!") print(f"{idx+1}. {rule.get('name', rule.get('file_name', ''))}")
return False return rules
r.raise_for_status()
content_type = r.headers.get('Content-Type', '')
if 'text/plain' in content_type:
html = r.content.decode(errors='replace')
import re
match = re.search(
r"<meta[^>]+http-equiv=['\"]refresh['\"][^>]+content=['\"]0;url=([^'\"]+)['\"]",
html,
re.IGNORECASE
)
if match:
real_url = match.group(1)
print(f"Found meta refresh redirect to: {real_url}")
with requests.get(real_url, stream=True, timeout=120) as r2:
r2.raise_for_status()
with open(out_path, 'wb') as f:
for chunk in r2.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Downloaded {out_path}")
return True
print("Error: Received HTML instead of file. Possible permission or quota issue.")
return False
with open(out_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Downloaded {out_path}")
return True
except Exception as e: except Exception as e:
print(f"Error downloading wordlist: {e}") print(f"Error fetching Hashmob rules: {e}")
return False return []
def download_hashmob_rule(file_name, out_path):
"""Download a rule file from Hashmob by file name."""
url = f"https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/{file_name}"
api_key = get_hashmob_api_key()
headers = {"api-key": api_key} if api_key else {}
import time
base_backoff = 256
max_backoff = 300
penalty_add = 2
penalty = base_backoff
import threading
lock = getattr(download_hashmob_rule, '_rate_lock', None)
if lock is None:
lock = threading.Lock()
download_hashmob_rule._rate_lock = lock
while True:
with lock:
time.sleep(15)
try:
with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r:
if r.status_code == 429:
print(f"[!] Rate limit hit (429). Backing off for {penalty} seconds...")
time.sleep(penalty)
penalty = min(penalty + penalty_add, max_backoff)
penalty_add *= 2
continue
r.raise_for_status()
with open(out_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Downloaded {out_path}")
return True
except Exception as e:
# If it's a 429 error, handle backoff, else fail
if hasattr(e, 'response') and getattr(e.response, 'status_code', None) == 429:
print(f"[!] Rate limit hit (429). Backing off for {penalty} seconds...")
time.sleep(penalty)
penalty = min(penalty + penalty_add, max_backoff)
penalty_add *= 2
continue
print(f"Error downloading rule: {e}")
return False
def list_official_wordlists(): def list_official_wordlists():
@@ -898,6 +1030,79 @@ def list_and_download_official_wordlists():
print(f"Error listing official wordlists: {e}") print(f"Error listing official wordlists: {e}")
def list_and_download_hashmob_rules():
"""List rules via the Hashmob API, prompt for selection, and download."""
rules = download_hashmob_rule_list()
if not rules:
return
print("a. Download ALL files")
sel = input("Enter the number(s) to download (e.g. 1,3,5-7), or 'a' for all, or 'q' to quit: ")
if sel.lower() == 'q':
return
rules_dir = get_rules_dir()
def parse_indices(selection, max_index):
indices = set()
for part in selection.split(','):
part = part.strip()
if not part:
continue
if '-' in part:
try:
start, end = map(int, part.split('-', 1))
if start > end:
start, end = end, start
indices.update(range(start, end + 1))
except Exception:
continue
else:
try:
indices.add(int(part))
except Exception:
continue
return sorted(i for i in indices if 1 <= i <= max_index)
# Track already-downloaded rules to avoid duplicates
downloaded_rules = set()
# Scan rules_dir for existing files
if os.path.isdir(rules_dir):
for fname in os.listdir(rules_dir):
downloaded_rules.add(fname)
def already_downloaded(file_name):
sanitized = sanitize_filename(file_name)
return sanitized in downloaded_rules
if sel.lower() == 'a':
for entry in rules:
file_name = entry.get('file_name')
if not file_name:
print("No file_name found for an entry, skipping.")
continue
out_path = os.path.join(rules_dir, sanitize_filename(file_name))
if already_downloaded(file_name):
print(f"[i] Skipping already downloaded rule: {file_name}")
continue
download_hashmob_rule(file_name, out_path)
return
indices = parse_indices(sel, len(rules))
if not indices:
print("No valid selection.")
return
for idx in indices:
entry = rules[idx - 1]
file_name = entry.get('file_name')
if not file_name:
print("No file_name found for selection, skipping.")
continue
out_path = os.path.join(rules_dir, sanitize_filename(file_name))
if already_downloaded(file_name):
print(f"[i] Skipping already downloaded rule: {file_name}")
continue
download_hashmob_rule(file_name, out_path)
def download_official_wordlist(file_name, out_path): def download_official_wordlist(file_name, out_path):
"""Download a file from the official wordlists directory with a progress bar.""" """Download a file from the official wordlists directory with a progress bar."""
import sys import sys
@@ -995,6 +1200,12 @@ def download_hashmob_wordlists(print_fn=print) -> None:
print_fn("Hashmob wordlist download complete.") print_fn("Hashmob wordlist download complete.")
def download_hashmob_rules(print_fn=print) -> None:
"""Download Hashmob rules."""
list_and_download_hashmob_rules()
print_fn("Hashmob rules download complete.")
def download_weakpass_torrent(download_torrent, filename: str, print_fn=print) -> None: def download_weakpass_torrent(download_torrent, filename: str, print_fn=print) -> None:
"""Download a single Weakpass torrent file by name or URL.""" """Download a single Weakpass torrent file by name or URL."""
print_fn(f"[i] Downloading: {filename}") print_fn(f"[i] Downloading: {filename}")