diff --git a/README.md b/README.md
index 19ef267..00a26b9 100644
--- a/README.md
+++ b/README.md
@@ -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.
+-------------------------------------------------------------------
+## 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
`$ ./hate_crack.py`
diff --git a/config.json.example b/config.json.example
index 0e05c1e..0fdbb5b 100644
--- a/config.json.example
+++ b/config.json.example
@@ -4,6 +4,7 @@
"hcatTuning": "--force --remove",
"hcatWordlists": "/Passwords/wordlists",
"hcatOptimizedWordlists": "/Passwords/optimized_wordlists",
+ "rules_directory": "/path/to/hashcat/rules",
"hcatDictionaryWordlist": ["rockyou.txt"],
"hcatCombinationWordlist": ["rockyou.txt","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",
"hashview_url": "http://localhost:8443",
"hashview_api_key": ""
-}
\ No newline at end of file
+}
diff --git a/hate_crack.py b/hate_crack.py
index 0b6a290..3f433d0 100755
--- a/hate_crack.py
+++ b/hate_crack.py
@@ -104,6 +104,7 @@ from hate_crack.api import (
download_all_weakpass_torrents,
download_hashes_from_hashview,
download_hashmob_wordlists,
+ download_hashmob_rules,
download_weakpass_torrent,
extract_with_7z,
)
@@ -120,6 +121,17 @@ hcatWordlists = config_parser['hcatWordlists']
hcatOptimizedWordlists = config_parser['hcatOptimizedWordlists']
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:
maxruntime = config_parser['bandrelmaxruntime']
except KeyError as e:
@@ -219,6 +231,38 @@ def verify_wordlist_dir(directory, wordlist):
print('Exiting. Please check configuration and try again.')
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():
wordlists_dir = hcatWordlists or os.path.join(hate_path, "wordlists")
if not os.path.isabs(wordlists_dir):
@@ -479,15 +523,16 @@ def hcatBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
def hcatDictionary(hcatHashType, hcatHashFile):
global hcatDictionaryCount
global hcatProcess
+ rule_best66 = get_rule_path("best66.rule")
hcatProcess = subprocess.Popen(
"{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(
- hcatPath=hcatPath,
+ "-r {rule_best66} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatBin=hcatBin,
hcatHashType=hcatHashType,
hash_file=hcatHashFile,
session_name=generate_session_id(),
optimized_wordlists=hcatOptimizedWordlists,
+ rule_best66=rule_best66,
tuning=hcatTuning,
hate_path=hate_path), shell=True)
try:
@@ -498,15 +543,16 @@ def hcatDictionary(hcatHashType, hcatHashFile):
for wordlist in hcatDictionaryWordlist:
+ rule_d3ad0ne = get_rule_path("d3ad0ne.rule")
hcatProcess = subprocess.Popen(
"{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(
- hcatPath=hcatPath,
+ "-r {rule_d3ad0ne} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatBin=hcatBin,
hcatHashType=hcatHashType,
hash_file=hcatHashFile,
session_name=generate_session_id(),
hcatWordlist=wordlist,
+ rule_d3ad0ne=rule_d3ad0ne,
tuning=hcatTuning,
hate_path=hate_path), shell=True)
try:
@@ -515,16 +561,16 @@ def hcatDictionary(hcatHashType, hcatHashFile):
print('Killing PID {0}...'.format(str(hcatProcess.pid)))
hcatProcess.kill()
-
+ rule_toxic = get_rule_path("T0XlC.rule")
hcatProcess = subprocess.Popen(
"{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(
- hcatPath=hcatPath,
+ "-r {rule_toxic} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatBin=hcatBin,
hcatHashType=hcatHashType,
hash_file=hcatHashFile,
session_name=generate_session_id(),
hcatWordlist=wordlist,
+ rule_toxic=rule_toxic,
tuning=hcatTuning,
hate_path=hate_path), shell=True)
try:
@@ -1037,16 +1083,19 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile):
def hcatPrince(hcatHashType, hcatHashFile):
global hcatProcess
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(
"{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 "
- "-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,
prince_bin=hcatPrinceBin,
hash_type=hcatHashType,
hash_file=hcatHashFile,
session_name=generate_session_id(),
hcatPrinceBaseList=hcatPrinceBaseList,
+ prince_rule=prince_rule,
tuning=hcatTuning,
hate_path=hate_path), shell=True)
try:
@@ -1059,16 +1108,18 @@ def hcatPrince(hcatHashType, hcatHashFile):
def hcatGoodMeasure(hcatHashType, hcatHashFile):
global hcatExtraCount
global hcatProcess
+ rule_combinator = get_rule_path("combinator.rule")
+ rule_insidepro = get_rule_path("InsidePro-PasswordsPro.rule")
hcatProcess = subprocess.Popen(
- "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -r {hcatPath}/rules/combinator.rule "
- "-r {hcatPath}/rules/InsidePro-PasswordsPro.rule {hcatGoodMeasureBaseList} {tuning} "
- "--potfile-path={hate_path}/hashcat.pot".format(
- hcatPath=hcatPath,
+ "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -r {rule_combinator} "
+ "-r {rule_insidepro} {hcatGoodMeasureBaseList} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatBin=hcatBin,
hash_type=hcatHashType,
hash_file=hcatHashFile,
hcatGoodMeasureBaseList=hcatGoodMeasureBaseList,
session_name=generate_session_id(),
+ rule_combinator=rule_combinator,
+ rule_insidepro=rule_insidepro,
tuning=hcatTuning,
hate_path=hate_path), shell=True)
try:
@@ -1136,10 +1187,11 @@ def hcatLMtoNT():
hcatProcess = subprocess.Popen(
"{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,
hash_file=hcatHashFile,
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,
hate_path=hate_path), shell=True)
try:
@@ -1170,15 +1222,15 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords):
with open(working_file, 'w') as f:
f.write("\n".join(converted))
for rule in hcatRules:
+ rule_path = get_rule_path(rule)
hcatProcess = subprocess.Popen(
"{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(
- rule=rule,
+ "-r {rule_path} {tuning} --potfile-path={hate_path}/hashcat.pot".format(
hcatBin=hcatBin,
hash_type=hcatHashType,
hash_file=hcatHashFile,
session_name=generate_session_id(),
- hcatPath=hcatPath,
+ rule_path=rule_path,
tuning=hcatTuning,
hate_path=hate_path), shell=True)
try:
@@ -1698,7 +1750,11 @@ def pipal():
print('Killing PID {0}...'.format(str(pipalProcess.pid)))
pipalProcess.kill()
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'):
print("\n--- Pipal Output Start ---\n")
with open(hcatHashFilePipal + ".pipal") as pipalfile:
@@ -1802,7 +1858,7 @@ def main():
global lmHashesFound
global debug_mode
global hashview_url, hashview_api_key
- global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists
+ global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists, rulesDirectory
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('--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('--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('--debug', action='store_true', help='Enable debug mode')
# Removed add_common_args(parser) since config items are now only set via config file
@@ -1837,6 +1894,7 @@ def main():
hcatBin=hcatBin,
hcatWordlists=hcatWordlists,
hcatOptimizedWordlists=hcatOptimizedWordlists,
+ rules_directory=rulesDirectory,
pipalPath=pipalPath,
maxruntime=maxruntime,
bandrelbasewords=bandrelbasewords,
@@ -1848,6 +1906,7 @@ def main():
hcatBin = config.hcatBin
hcatWordlists = config.hcatWordlists
hcatOptimizedWordlists = config.hcatOptimizedWordlists
+ rulesDirectory = config.rules_directory
pipalPath = config.pipalPath
maxruntime = config.maxruntime
bandrelbasewords = config.bandrelbasewords
@@ -1905,6 +1964,9 @@ def main():
if args.hashmob:
download_hashmob_wordlists(print_fn=print)
sys.exit(0)
+ if args.rules:
+ download_hashmob_rules(print_fn=print)
+ sys.exit(0)
if args.hashfile and args.hashtype:
hcatHashFile = resolve_path(args.hashfile)
@@ -1923,7 +1985,8 @@ def main():
print("\t(1) Download hashes from Hashview")
print("\t(2) Download wordlists from Weakpass")
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: ")
if choice == '1' or args.download_hashview:
if not hashview_api_key:
@@ -1950,6 +2013,9 @@ def main():
elif choice == '3' or args.hashmob:
download_hashmob_wordlists(print_fn=print)
sys.exit(0)
+ elif choice == '4' or args.rules:
+ download_hashmob_rules(print_fn=print)
+ sys.exit(0)
else:
sys.exit(0)
@@ -2031,6 +2097,7 @@ def main():
"11": middle_combinator,
"12": thorough_combinator,
"13": bandrel_method,
+ "91": download_hashmob_rules,
"92": weakpass_wordlist_menu,
"93": download_hashmob_wordlists,
"94": weakpass_wordlist_menu,
@@ -2055,6 +2122,7 @@ def main():
print("\t(11) Middle Combinator Attack")
print("\t(12) Thorough Combinator Attack")
print("\t(13) Bandrel Methodology")
+ print("\n\t(91) Download rules from Hashmob.net")
print("\n\t(92) Download wordlists from Weakpass")
print("\t(93) Download wordlists from Hashmob.net")
print("\t(94) Weakpass Wordlist Menu")
diff --git a/hate_crack/api.py b/hate_crack/api.py
index 724af6b..ba1e1f9 100644
--- a/hate_crack/api.py
+++ b/hate_crack/api.py
@@ -57,6 +57,31 @@ def get_hcat_wordlists_dir():
os.makedirs(default, exist_ok=True)
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):
"""Remove stray .torrent files from the wordlists directory on graceful exit."""
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):
save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), save_dir)
os.makedirs(save_dir, exist_ok=True)
+ # Optionally include hashmob_api_key in headers if present
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"
}
+ 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.
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}"
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_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"]+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:
- with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r:
- 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"]+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
+ resp = requests.get(url, headers=headers, timeout=30)
+ resp.raise_for_status()
+ data = resp.json()
+ rules = [r for r in data if r.get('type') in ('rule', 'official_rule')]
+ print("Available Hashmob Rules:")
+ for idx, rule in enumerate(rules):
+ print(f"{idx+1}. {rule.get('name', rule.get('file_name', ''))}")
+ return rules
except Exception as e:
- print(f"Error downloading wordlist: {e}")
- return False
+ print(f"Error fetching Hashmob rules: {e}")
+ 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():
@@ -898,6 +1030,79 @@ def list_and_download_official_wordlists():
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):
"""Download a file from the official wordlists directory with a progress bar."""
import sys
@@ -995,6 +1200,12 @@ def download_hashmob_wordlists(print_fn=print) -> None:
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:
"""Download a single Weakpass torrent file by name or URL."""
print_fn(f"[i] Downloading: {filename}")