mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-04 19:57:22 -07:00
Merge remote-tracking branch 'refs/remotes/origin/main'
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
config.json
|
||||
hashcat.pot
|
||||
hate_crack/__init__.pyc
|
||||
__pycache__/
|
||||
hate_crack/__pycache__/
|
||||
tests/__pycache__/
|
||||
uv.lock
|
||||
|
||||
@@ -34,6 +34,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`
|
||||
|
||||
+2
-1
@@ -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": ""
|
||||
}
|
||||
}
|
||||
|
||||
+87
-19
@@ -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:
|
||||
@@ -1703,7 +1755,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:
|
||||
@@ -1807,7 +1863,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
|
||||
|
||||
|
||||
@@ -1823,6 +1879,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
|
||||
@@ -1842,6 +1899,7 @@ def main():
|
||||
hcatBin=hcatBin,
|
||||
hcatWordlists=hcatWordlists,
|
||||
hcatOptimizedWordlists=hcatOptimizedWordlists,
|
||||
rules_directory=rulesDirectory,
|
||||
pipalPath=pipalPath,
|
||||
maxruntime=maxruntime,
|
||||
bandrelbasewords=bandrelbasewords,
|
||||
@@ -1853,6 +1911,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
|
||||
@@ -1910,6 +1969,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)
|
||||
@@ -1928,7 +1990,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:
|
||||
@@ -1955,6 +2018,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)
|
||||
|
||||
@@ -2036,6 +2102,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,
|
||||
@@ -2060,6 +2127,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")
|
||||
|
||||
+250
-39
@@ -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"):
|
||||
@@ -777,47 +820,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"<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:
|
||||
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"<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
|
||||
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():
|
||||
@@ -916,6 +1048,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
|
||||
@@ -1013,6 +1218,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}")
|
||||
|
||||
Reference in New Issue
Block a user