mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
Shard Wordlist (option 7) now prompts for an output base path and a shard count, then writes all N interleaved parts as base.001..base.00N in a single pass instead of one file per modulus/offset invocation. Matches the intended distributed-cracking workflow: split once, copy one part per node. Updates tests and README usage docs; bumps CHANGELOG to 2.10.11. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1278 lines
45 KiB
Python
1278 lines
45 KiB
Python
import glob
|
|
import gzip
|
|
import os
|
|
import readline
|
|
from typing import Any
|
|
|
|
from hate_crack import notify as _notify
|
|
from hate_crack.api import download_hashmob_rules
|
|
from hate_crack.formatting import print_multicolumn_list
|
|
from hate_crack.menu import interactive_menu
|
|
|
|
|
|
def _configure_readline(completer):
|
|
readline.set_completer_delims(" \t\n;")
|
|
try:
|
|
readline.parse_and_bind("set completion-query-items -1")
|
|
except Exception:
|
|
pass
|
|
try:
|
|
readline.parse_and_bind("tab: complete")
|
|
except Exception:
|
|
pass
|
|
try:
|
|
readline.parse_and_bind("bind ^I rl_complete")
|
|
except Exception:
|
|
pass
|
|
readline.set_completer(completer)
|
|
|
|
|
|
def _select_rules(ctx) -> list[str] | None:
|
|
"""Prompt user to select rules. Returns list of rule chain strings, or None if cancelled."""
|
|
rule_choice = None
|
|
selected_rules = []
|
|
|
|
rules_dir = ctx.rulesDirectory
|
|
rule_files = sorted(f for f in os.listdir(rules_dir) if f != ".DS_Store")
|
|
if not rule_files:
|
|
download_rules = (
|
|
input("\nNo rules found. Download rules from Hashmob now? (Y/n): ")
|
|
.strip()
|
|
.lower()
|
|
)
|
|
if download_rules in ("", "y", "yes"):
|
|
download_hashmob_rules(print_fn=print, rules_dir=rules_dir)
|
|
rule_files = sorted(os.listdir(rules_dir))
|
|
|
|
if not rule_files:
|
|
print("No rules available. Proceeding without rules.")
|
|
return [""]
|
|
|
|
print("\nWhich rule(s) would you like to run?")
|
|
rule_entries = ["0) To run without any rules"]
|
|
rule_entries.extend([f"{i}) {file}" for i, file in enumerate(rule_files, start=1)])
|
|
rule_entries.append("98) YOLO...run all of the rules")
|
|
rule_entries.append("99) Back to Main Menu")
|
|
max_rule_len = max((len(e) for e in rule_entries), default=26)
|
|
print_multicolumn_list(
|
|
"Available Rules",
|
|
rule_entries,
|
|
min_col_width=max_rule_len,
|
|
max_col_width=max_rule_len,
|
|
)
|
|
|
|
example_line = ""
|
|
if len(rule_files) >= 2:
|
|
example_line = f"For example 1+1 will run {rule_files[0]} chained twice and 1,2 would run {rule_files[0]} and then {rule_files[1]} sequentially.\n"
|
|
elif len(rule_files) == 1:
|
|
example_line = f"For example 1+1 will run {rule_files[0]} chained twice.\n"
|
|
|
|
while rule_choice is None:
|
|
raw_choice = input(
|
|
"Enter Comma separated list of rules you would like to run. To run rules chained use the + symbol.\n"
|
|
f"{example_line}"
|
|
"Choose wisely: "
|
|
)
|
|
if raw_choice.strip() == "99":
|
|
return None
|
|
if raw_choice != "":
|
|
tokens = raw_choice.split(",")
|
|
expanded = []
|
|
for tok in tokens:
|
|
tok = tok.strip()
|
|
if "+" not in tok and "-" in tok:
|
|
parts = tok.split("-", 1)
|
|
try:
|
|
start, end = int(parts[0]), int(parts[1])
|
|
if start <= end:
|
|
expanded.extend(str(i) for i in range(start, end + 1))
|
|
continue
|
|
except ValueError:
|
|
pass
|
|
expanded.append(tok)
|
|
rule_choice = expanded
|
|
|
|
if "99" in rule_choice:
|
|
return None
|
|
if "98" in rule_choice:
|
|
for rule in rule_files:
|
|
selected_rules.append(f"-r {os.path.join(rules_dir, rule)}")
|
|
elif "0" in rule_choice:
|
|
selected_rules = [""]
|
|
else:
|
|
for choice in rule_choice:
|
|
if "+" in choice:
|
|
combined_choice = ""
|
|
choices = choice.split("+")
|
|
for rule in choices:
|
|
try:
|
|
rule_path = os.path.join(rules_dir, rule_files[int(rule) - 1])
|
|
combined_choice = f"{combined_choice} -r {rule_path}"
|
|
except Exception:
|
|
continue
|
|
selected_rules.append(combined_choice)
|
|
else:
|
|
try:
|
|
rule_path = os.path.join(rules_dir, rule_files[int(choice) - 1])
|
|
selected_rules.append(f"-r {rule_path}")
|
|
except (IndexError, ValueError):
|
|
continue
|
|
|
|
return selected_rules
|
|
|
|
|
|
def quick_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Quick Crack")
|
|
wordlist_choice = None
|
|
list_dir = ctx.hcatWordlists
|
|
default_dir = ctx.hcatOptimizedWordlists
|
|
|
|
wordlist_files = ctx.list_wordlist_files(list_dir)
|
|
wordlist_entries = [
|
|
f"{i}) {file}" for i, file in enumerate(wordlist_files, start=1)
|
|
]
|
|
max_entry_len = max((len(e) for e in wordlist_entries), default=24)
|
|
print_multicolumn_list(
|
|
"Wordlists",
|
|
wordlist_entries,
|
|
min_col_width=max_entry_len,
|
|
max_col_width=max_entry_len,
|
|
)
|
|
|
|
def path_completer(text, state):
|
|
base = list_dir
|
|
if not text:
|
|
pattern = os.path.join(base, "*")
|
|
matches = glob.glob(pattern)
|
|
else:
|
|
text = os.path.expanduser(text)
|
|
if text.startswith(("/", "./", "../", "~")):
|
|
matches = glob.glob(text + "*")
|
|
else:
|
|
pattern = os.path.join(base, text + "*")
|
|
matches = glob.glob(pattern)
|
|
matches = [m + "/" if os.path.isdir(m) else m for m in matches]
|
|
try:
|
|
return matches[state]
|
|
except IndexError:
|
|
return None
|
|
|
|
_configure_readline(path_completer)
|
|
|
|
while wordlist_choice is None:
|
|
try:
|
|
raw_choice = input(
|
|
"\nEnter path of wordlist or wordlist directory (tab to autocomplete).\n"
|
|
f"Press Enter for default wordlist directory [{default_dir}]: "
|
|
)
|
|
raw_choice = raw_choice.strip()
|
|
if raw_choice == "":
|
|
wordlist_choice = default_dir
|
|
elif raw_choice.isdigit() and 1 <= int(raw_choice) <= len(wordlist_files):
|
|
chosen = os.path.join(
|
|
list_dir, wordlist_files[int(raw_choice) - 1]
|
|
)
|
|
if os.path.exists(chosen):
|
|
wordlist_choice = chosen
|
|
print(wordlist_choice)
|
|
elif os.path.exists(raw_choice):
|
|
wordlist_choice = raw_choice
|
|
else:
|
|
wordlist_choice = None
|
|
print("Please enter a valid wordlist or wordlist directory.")
|
|
except ValueError:
|
|
print("Please enter a valid number.")
|
|
|
|
selected_rules = _select_rules(ctx)
|
|
if selected_rules is None:
|
|
return
|
|
|
|
for chain in selected_rules:
|
|
ctx.hcatQuickDictionary(
|
|
ctx.hcatHashType,
|
|
ctx.hcatHashFile,
|
|
chain,
|
|
wordlist_choice,
|
|
attack_name="Quick Crack",
|
|
)
|
|
|
|
|
|
def loopback_attack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Loopback")
|
|
empty_wordlist = os.path.join(ctx.hcatWordlists, "empty.txt")
|
|
os.makedirs(ctx.hcatWordlists, exist_ok=True)
|
|
if not os.path.exists(empty_wordlist):
|
|
with open(empty_wordlist, "w"):
|
|
pass
|
|
|
|
print(f"\nUsing loopback attack with wordlist: {empty_wordlist}")
|
|
|
|
selected_rules = _select_rules(ctx)
|
|
if selected_rules is None:
|
|
return
|
|
|
|
for chain in selected_rules:
|
|
ctx.hcatQuickDictionary(
|
|
ctx.hcatHashType,
|
|
ctx.hcatHashFile,
|
|
chain,
|
|
empty_wordlist,
|
|
loopback=True,
|
|
attack_name="Loopback",
|
|
)
|
|
|
|
|
|
def extensive_crack(ctx: Any) -> None:
|
|
# Orchestrator attack: chains ~14 primitives. We suppress each primitive's
|
|
# own notifications and fire exactly one "Extensive Crack complete" at the
|
|
# end with the aggregate delta. This both prevents notification spam and
|
|
# gives the user an actually-useful summary.
|
|
_notify.prompt_notify_for_attack("Extensive Crack")
|
|
out_path = ctx.hcatHashFile + ".out"
|
|
cracked_before = ctx.lineCount(out_path) if os.path.exists(out_path) else 0
|
|
with _notify.suppressed_notifications():
|
|
ctx.hcatBruteForce(ctx.hcatHashType, ctx.hcatHashFile, "1", "7")
|
|
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatBruteCount)
|
|
ctx.hcatDictionary(ctx.hcatHashType, ctx.hcatHashFile)
|
|
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatDictionaryCount)
|
|
hcatTargetTime = 4 * 60 * 60
|
|
ctx.hcatTopMask(ctx.hcatHashType, ctx.hcatHashFile, hcatTargetTime)
|
|
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatMaskCount)
|
|
ctx.hcatFingerprint(
|
|
ctx.hcatHashType, ctx.hcatHashFile, 7, run_hybrid_on_expanded=False
|
|
)
|
|
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatFingerprintCount)
|
|
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile)
|
|
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatCombinationCount)
|
|
ctx.hcatHybrid(ctx.hcatHashType, ctx.hcatHashFile)
|
|
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatHybridCount)
|
|
ctx.hcatGoodMeasure(ctx.hcatHashType, ctx.hcatHashFile)
|
|
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatExtraCount)
|
|
cracked_after = ctx.lineCount(out_path) if os.path.exists(out_path) else 0
|
|
_notify.notify_job_done(
|
|
"Extensive Crack", cracked_after, ctx.hcatHashFile
|
|
)
|
|
# Note: ``cracked_before`` is tracked for potential future per-orchestrator
|
|
# delta reporting, but today the notify message uses the absolute count
|
|
# because that matches what single-attack notifications already report.
|
|
_ = cracked_before
|
|
|
|
|
|
def brute_force_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Brute Force")
|
|
hcatMinLen = int(
|
|
input("\nEnter the minimum password length to brute force (1): ") or 1
|
|
)
|
|
hcatMaxLen = int(
|
|
input("\nEnter the maximum password length to brute force (7): ") or 7
|
|
)
|
|
ctx.hcatBruteForce(ctx.hcatHashType, ctx.hcatHashFile, hcatMinLen, hcatMaxLen)
|
|
|
|
|
|
def top_mask_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Top Mask")
|
|
hcatTargetTime = int(
|
|
input("\nEnter a target time for completion in hours (4): ") or 4
|
|
)
|
|
hcatTargetTime = hcatTargetTime * 60 * 60
|
|
ctx.hcatTopMask(ctx.hcatHashType, ctx.hcatHashFile, hcatTargetTime)
|
|
|
|
|
|
def fingerprint_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Fingerprint")
|
|
while True:
|
|
raw = input("\nEnter expander max length (7-36) (7): ").strip()
|
|
if raw == "":
|
|
expander_len = 7
|
|
break
|
|
try:
|
|
expander_len = int(raw)
|
|
except ValueError:
|
|
print("Please enter an integer between 7 and 36.")
|
|
continue
|
|
if 7 <= expander_len <= 36:
|
|
break
|
|
print("Please enter an integer between 7 and 36.")
|
|
|
|
ctx.hcatFingerprint(
|
|
ctx.hcatHashType,
|
|
ctx.hcatHashFile,
|
|
expander_len,
|
|
run_hybrid_on_expanded=True,
|
|
)
|
|
|
|
|
|
def combinator_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Combinator")
|
|
print("\n" + "=" * 60)
|
|
print("COMBINATOR ATTACK")
|
|
print("=" * 60)
|
|
print("Combines 2-8 wordlists. 2 uses hashcat native mode; 3+ use external binaries.")
|
|
print("=" * 60)
|
|
|
|
use_default = (
|
|
input("\nUse default combinator wordlists from config? (Y/n): ").strip().lower()
|
|
)
|
|
|
|
if use_default != "n":
|
|
base = ctx.hcatCombinationWordlist
|
|
wordlists = base if isinstance(base, list) else [base]
|
|
wordlists = [ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists]
|
|
if len(wordlists) < 2:
|
|
print("\n[!] Config does not have at least 2 wordlists.")
|
|
print("Set hcatCombinationWordlist to a list of 2+ paths in config.json.")
|
|
print("Aborting combinator attack.")
|
|
return
|
|
separator = ""
|
|
else:
|
|
print("\nEnter 2-8 wordlists. Enter a blank line when done.")
|
|
wordlists = _prompt_wordlist_paths(ctx, max_count=8)
|
|
if len(wordlists) < 2:
|
|
print("\n[!] Combinator attack requires at least 2 wordlists.")
|
|
print("Aborting combinator attack.")
|
|
return
|
|
separator = input("\nEnter separator between words (leave blank for none): ").strip()
|
|
|
|
if len(wordlists) == 2 and not separator:
|
|
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
|
elif len(wordlists) == 3 and not separator:
|
|
ctx.hcatCombinator3(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
|
else:
|
|
ctx.hcatCombinatorX(ctx.hcatHashType, ctx.hcatHashFile, wordlists, separator or None)
|
|
|
|
|
|
def hybrid_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Hybrid")
|
|
print("\n" + "=" * 60)
|
|
print("HYBRID ATTACK")
|
|
print("=" * 60)
|
|
print("This attack combines wordlists with masks to generate candidates.")
|
|
print("Examples:")
|
|
print(" - Mode 6: wordlist + mask (e.g., 'password' + '123')")
|
|
print(" - Mode 7: mask + wordlist (e.g., '123' + 'password')")
|
|
print("=" * 60)
|
|
|
|
use_default = (
|
|
input("\nUse default hybrid wordlist from config? (Y/n): ").strip().lower()
|
|
)
|
|
|
|
if use_default != "n":
|
|
print("\nUsing default wordlist(s) from config:")
|
|
if isinstance(ctx.hcatHybridlist, list):
|
|
for wl in ctx.hcatHybridlist:
|
|
print(f" - {wl}")
|
|
wordlists = ctx.hcatHybridlist
|
|
else:
|
|
print(f" - {ctx.hcatHybridlist}")
|
|
wordlists = [ctx.hcatHybridlist]
|
|
else:
|
|
print("\nSelect wordlist(s) for hybrid attack.")
|
|
print("You can enter:")
|
|
print(" - A single file path")
|
|
print(" - Multiple paths separated by commas")
|
|
print(" - Press TAB to autocomplete file paths")
|
|
|
|
selection = ctx.select_file_with_autocomplete(
|
|
"Enter wordlist file(s) (comma-separated for multiple)",
|
|
allow_multiple=True,
|
|
base_dir=ctx.hcatWordlists,
|
|
)
|
|
|
|
if not selection:
|
|
print("No wordlist selected. Aborting hybrid attack.")
|
|
return
|
|
|
|
if isinstance(selection, str):
|
|
wordlists = [selection]
|
|
else:
|
|
wordlists = selection
|
|
|
|
valid_wordlists = []
|
|
for wl in wordlists:
|
|
resolved = ctx._resolve_wordlist_path(wl, ctx.hcatWordlists)
|
|
if os.path.isfile(resolved):
|
|
valid_wordlists.append(resolved)
|
|
print(f"✓ Found: {resolved}")
|
|
else:
|
|
print(f"✗ Not found: {resolved}")
|
|
|
|
if not valid_wordlists:
|
|
print("\nNo valid wordlists found. Aborting hybrid attack.")
|
|
return
|
|
|
|
wordlists = valid_wordlists
|
|
wordlists = [ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists]
|
|
|
|
print(f"\nStarting hybrid attack with {len(wordlists)} wordlist(s)...")
|
|
print(f"Hash type: {ctx.hcatHashType}")
|
|
print(f"Hash file: {ctx.hcatHashFile}")
|
|
|
|
ctx.hcatHybrid(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
|
|
|
|
|
def pathwell_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Pathwell Brute Force")
|
|
ctx.hcatPathwellBruteForce(ctx.hcatHashType, ctx.hcatHashFile)
|
|
|
|
|
|
def prince_attack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("PRINCE")
|
|
ctx.hcatPrince(ctx.hcatHashType, ctx.hcatHashFile)
|
|
|
|
|
|
def pcfg_attack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("PCFG")
|
|
ctx.hcatPCFG(ctx.hcatHashType, ctx.hcatHashFile)
|
|
|
|
|
|
def prince_ling_attack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("PRINCE-LING")
|
|
ctx.hcatPrinceLing(ctx.hcatHashType, ctx.hcatHashFile)
|
|
|
|
|
|
def yolo_combination(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("YOLO Combination")
|
|
ctx.hcatYoloCombination(ctx.hcatHashType, ctx.hcatHashFile)
|
|
|
|
|
|
def thorough_combinator(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Thorough Combinator")
|
|
ctx.hcatThoroughCombinator(ctx.hcatHashType, ctx.hcatHashFile)
|
|
|
|
|
|
def middle_combinator(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Middle Combinator")
|
|
ctx.hcatMiddleCombinator(ctx.hcatHashType, ctx.hcatHashFile)
|
|
|
|
|
|
def _prompt_wordlist_paths(ctx, max_count: int) -> list[str]:
|
|
"""Prompt for wordlist paths one at a time with tab-autocomplete.
|
|
|
|
Stops when a blank line is entered or max_count paths have been collected.
|
|
Returns a list of resolved, valid file paths.
|
|
"""
|
|
|
|
def path_completer(text, state):
|
|
base = ctx.hcatWordlists
|
|
if not text:
|
|
pattern = os.path.join(base, "*")
|
|
matches = glob.glob(pattern)
|
|
else:
|
|
expanded = os.path.expanduser(text)
|
|
if expanded.startswith(("/", "./", "../", "~")):
|
|
matches = glob.glob(expanded + "*")
|
|
else:
|
|
pattern = os.path.join(base, expanded + "*")
|
|
matches = glob.glob(pattern)
|
|
matches = [m + "/" if os.path.isdir(m) else m for m in matches]
|
|
try:
|
|
return matches[state]
|
|
except IndexError:
|
|
return None
|
|
|
|
_configure_readline(path_completer)
|
|
|
|
collected: list[str] = []
|
|
count = 1
|
|
while len(collected) < max_count:
|
|
raw = input(
|
|
f"\nWordlist #{count} (tab to autocomplete, blank to finish): "
|
|
).strip()
|
|
if not raw:
|
|
break
|
|
resolved = ctx._resolve_wordlist_path(raw, ctx.hcatWordlists)
|
|
if os.path.isfile(resolved):
|
|
collected.append(resolved)
|
|
print(f"Added: {resolved}")
|
|
count += 1
|
|
else:
|
|
print(f"Not found: {resolved}")
|
|
return collected
|
|
|
|
|
|
def combinator3_crack(ctx: Any) -> None:
|
|
"""3-way combinator attack (delegates to unified combinator_crack)."""
|
|
combinator_crack(ctx)
|
|
|
|
|
|
def combinatorX_crack(ctx: Any) -> None:
|
|
"""N-way combinator attack (delegates to unified combinator_crack)."""
|
|
combinator_crack(ctx)
|
|
|
|
|
|
def combinator_3plus_crack(ctx: Any) -> None:
|
|
"""3+ wordlist combinator (delegates to unified combinator_crack)."""
|
|
combinator_crack(ctx)
|
|
|
|
|
|
def bandrel_method(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Bandrel")
|
|
ctx.hcatBandrel(ctx.hcatHashType, ctx.hcatHashFile)
|
|
|
|
|
|
def ollama_attack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("LLM")
|
|
print("\n\tLLM Attack")
|
|
company = input("Company name: ").strip()
|
|
industry = input("Industry: ").strip()
|
|
location = input("Location: ").strip()
|
|
target_info = {
|
|
"company": company,
|
|
"industry": industry,
|
|
"location": location,
|
|
}
|
|
ctx.hcatOllama(ctx.hcatHashType, ctx.hcatHashFile, "target", target_info)
|
|
|
|
|
|
def _omen_pick_training_wordlist(ctx: Any):
|
|
"""Show wordlist picker for OMEN training. Returns path or None."""
|
|
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
|
|
if wordlist_files:
|
|
entries = [f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)]
|
|
max_len = max((len(e) for e in entries), default=24)
|
|
print_multicolumn_list(
|
|
"Training Wordlists",
|
|
entries,
|
|
min_col_width=max_len,
|
|
max_col_width=max_len,
|
|
)
|
|
print("\tp. Enter a custom path")
|
|
sel = input("\n\tSelect wordlist for training: ").strip()
|
|
if sel.lower() == "p":
|
|
path = input("\n\tPath to training wordlist: ").strip()
|
|
return path if path else None
|
|
try:
|
|
idx = int(sel)
|
|
if 1 <= idx <= len(wordlist_files):
|
|
return os.path.join(ctx.hcatWordlists, wordlist_files[idx - 1])
|
|
except (ValueError, IndexError):
|
|
pass
|
|
print("\t[!] Invalid selection.")
|
|
return None
|
|
|
|
|
|
def omen_attack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("OMEN")
|
|
print("\n\tOMEN Attack (Ordered Markov ENumerator)")
|
|
omen_dir = os.path.join(ctx.hate_path, "omen")
|
|
create_bin = os.path.join(omen_dir, ctx.hcatOmenCreateBin)
|
|
enum_bin = os.path.join(omen_dir, ctx.hcatOmenEnumBin)
|
|
if not os.path.isfile(create_bin) or not os.path.isfile(enum_bin):
|
|
print("\n\tOMEN binaries not found. Build them with:")
|
|
print(f"\t cd {omen_dir} && make")
|
|
return
|
|
|
|
model_dir = ctx._omen_model_dir()
|
|
model_valid = ctx._omen_model_is_valid(model_dir)
|
|
need_training = True
|
|
|
|
if model_valid:
|
|
info = ctx._omen_model_info(model_dir)
|
|
trained_with = info.get("training_file", "unknown") if info else "unknown"
|
|
print(f"\n\tOMEN model found (trained with: {trained_with})")
|
|
print("\t1. Use existing model")
|
|
print("\t2. Train new model (overwrites existing)")
|
|
print("\t3. Cancel")
|
|
choice = input("\n\tChoice: ").strip()
|
|
if choice == "1":
|
|
need_training = False
|
|
elif choice == "3":
|
|
return
|
|
elif choice != "2":
|
|
return
|
|
else:
|
|
print("\n\tNo valid OMEN model found. Training is required.")
|
|
|
|
if need_training:
|
|
training_file = _omen_pick_training_wordlist(ctx)
|
|
if not training_file:
|
|
return
|
|
if not ctx.hcatOmenTrain(training_file):
|
|
print("\n\t[!] Training failed. Aborting OMEN attack.")
|
|
return
|
|
|
|
max_candidates = input(
|
|
f"\n\tMax candidates to generate ({ctx.omenMaxCandidates}): "
|
|
).strip()
|
|
if not max_candidates:
|
|
max_candidates = str(ctx.omenMaxCandidates)
|
|
|
|
selected_rules = _select_rules(ctx)
|
|
if selected_rules is None:
|
|
return
|
|
|
|
for chain in selected_rules:
|
|
ctx.hcatOmen(ctx.hcatHashType, ctx.hcatHashFile, int(max_candidates), chain)
|
|
|
|
|
|
def _markov_pick_training_source(ctx: Any):
|
|
"""Prompt user to select markov training source. Returns file path or None."""
|
|
out_path = f"{ctx.hcatHashFile}.out"
|
|
has_cracked = os.path.isfile(out_path) and os.path.getsize(out_path) > 0
|
|
|
|
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
|
|
entries = []
|
|
if has_cracked:
|
|
entries.append("0) Cracked passwords (current session)")
|
|
entries.extend([f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)])
|
|
if entries:
|
|
max_len = max((len(e) for e in entries), default=24)
|
|
print_multicolumn_list(
|
|
"Markov Training Source",
|
|
entries,
|
|
min_col_width=max_len,
|
|
max_col_width=max_len,
|
|
)
|
|
print("\tp. Enter a custom path")
|
|
sel = input("\n\tSelect training source: ").strip()
|
|
if sel == "0" and has_cracked:
|
|
return out_path
|
|
if sel.lower() == "p":
|
|
path = input("\n\tPath to training file: ").strip()
|
|
return path if path else None
|
|
try:
|
|
idx = int(sel)
|
|
if 1 <= idx <= len(wordlist_files):
|
|
return os.path.join(ctx.hcatWordlists, wordlist_files[idx - 1])
|
|
except (ValueError, IndexError):
|
|
pass
|
|
print("\t[!] Invalid selection.")
|
|
return None
|
|
|
|
|
|
def adhoc_mask_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Ad-hoc Mask")
|
|
print(
|
|
"\nEnter a hashcat mask. Tokens: ?l=lower ?u=upper ?d=digit ?s=special ?a=all ?b=binary ?1-?4=custom"
|
|
)
|
|
mask = input("Mask (e.g. ?u?l?l?l?d?d): ").strip()
|
|
if not mask:
|
|
return
|
|
|
|
charset_flags = []
|
|
for i in range(1, 5):
|
|
cs = input(f"Custom charset -{i} [leave blank to skip]: ").strip()
|
|
if cs:
|
|
charset_flags.extend([f"-{i}", cs])
|
|
else:
|
|
break
|
|
|
|
ctx.hcatAdHocMask(
|
|
ctx.hcatHashType,
|
|
ctx.hcatHashFile,
|
|
mask,
|
|
" ".join(charset_flags),
|
|
)
|
|
|
|
|
|
def markov_brute_force(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Markov Brute Force")
|
|
print("\n\tMarkov Brute Force Attack")
|
|
hcstat2_path = f"{ctx.hcatHashFile}.hcstat2"
|
|
need_training = True
|
|
|
|
if os.path.isfile(hcstat2_path):
|
|
print(f"\n\tMarkov table found: {hcstat2_path}")
|
|
print("\t1. Use existing table")
|
|
print("\t2. Generate new table (overwrites existing)")
|
|
print("\t3. Cancel")
|
|
choice = input("\n\tChoice: ").strip()
|
|
if choice == "1":
|
|
need_training = False
|
|
elif choice == "3":
|
|
return
|
|
elif choice != "2":
|
|
return
|
|
else:
|
|
print("\n\tNo markov table found. Generation is required.")
|
|
|
|
if need_training:
|
|
source = _markov_pick_training_source(ctx)
|
|
if not source:
|
|
return
|
|
if not ctx.hcatMarkovTrain(source, ctx.hcatHashFile):
|
|
print("\n\t[!] Markov table generation failed. Aborting.")
|
|
return
|
|
|
|
hcatMinLen = int(
|
|
input("\nEnter the minimum password length to brute force (1): ") or 1
|
|
)
|
|
hcatMaxLen = int(
|
|
input("\nEnter the maximum password length to brute force (7): ") or 7
|
|
)
|
|
ctx.hcatMarkovBruteForce(ctx.hcatHashType, ctx.hcatHashFile, hcatMinLen, hcatMaxLen)
|
|
|
|
|
|
def combipow_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Combipow")
|
|
wordlist = None
|
|
while wordlist is None:
|
|
path = input("\n[*] Enter path to wordlist (max 63 lines recommended): ").strip()
|
|
if not path:
|
|
continue
|
|
if not os.path.isfile(path):
|
|
print(f"[!] File not found: {path}")
|
|
continue
|
|
with (gzip.open(path, "rb") if path.endswith(".gz") else open(path, "rb")) as fh:
|
|
line_count = sum(1 for _ in fh)
|
|
if line_count > 63:
|
|
print(
|
|
f"[!] Wordlist has {line_count} lines (max 63). combipow generates 2^n-1 combinations."
|
|
)
|
|
return
|
|
if line_count > 20:
|
|
print(
|
|
f"[*] Warning: {line_count} lines will generate a large number of combinations."
|
|
)
|
|
wordlist = path
|
|
use_space_sep = input("[*] Add spaces between words? (Y/n): ").strip().lower() != "n"
|
|
ctx.hcatCombipow(ctx.hcatHashType, ctx.hcatHashFile, wordlist, use_space_sep)
|
|
|
|
|
|
def generate_rules_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Random Rules")
|
|
print("\n" + "=" * 60)
|
|
print("RANDOM RULES ATTACK")
|
|
print("=" * 60)
|
|
print("Generates random hashcat mutation rules and applies them to a wordlist.")
|
|
print("Use when known rulesets are exhausted - a chaos mode for rule-space exploration.")
|
|
print("=" * 60)
|
|
|
|
raw_count = input("\nNumber of random rules to generate (65536): ").strip()
|
|
try:
|
|
rule_count = int(raw_count) if raw_count else 65536
|
|
if rule_count < 1:
|
|
print("[!] Rule count must be at least 1.")
|
|
return
|
|
except ValueError:
|
|
print("[!] Invalid rule count.")
|
|
return
|
|
|
|
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
|
|
wordlist_entries = [
|
|
f"{i}) {file}" for i, file in enumerate(wordlist_files, start=1)
|
|
]
|
|
max_entry_len = max((len(e) for e in wordlist_entries), default=24)
|
|
print_multicolumn_list(
|
|
"Wordlists",
|
|
wordlist_entries,
|
|
min_col_width=max_entry_len,
|
|
max_col_width=max_entry_len,
|
|
)
|
|
|
|
def path_completer(text, state):
|
|
base = ctx.hcatWordlists
|
|
if not text:
|
|
pattern = os.path.join(base, "*")
|
|
matches = glob.glob(pattern)
|
|
else:
|
|
text = os.path.expanduser(text)
|
|
if text.startswith(("/", "./", "../", "~")):
|
|
matches = glob.glob(text + "*")
|
|
else:
|
|
pattern = os.path.join(base, text + "*")
|
|
matches = glob.glob(pattern)
|
|
matches = [m + "/" if os.path.isdir(m) else m for m in matches]
|
|
try:
|
|
return matches[state]
|
|
except IndexError:
|
|
return None
|
|
|
|
_configure_readline(path_completer)
|
|
|
|
wordlist_choice = None
|
|
while wordlist_choice is None:
|
|
try:
|
|
raw_choice = input(
|
|
"\nEnter path of wordlist (tab to autocomplete).\n"
|
|
f"Press Enter for default wordlist directory [{ctx.hcatWordlists}]: "
|
|
)
|
|
raw_choice = raw_choice.strip()
|
|
if raw_choice == "":
|
|
wordlist_choice = ctx.hcatWordlists
|
|
elif raw_choice.isdigit() and 1 <= int(raw_choice) <= len(wordlist_files):
|
|
chosen = os.path.join(
|
|
ctx.hcatWordlists, wordlist_files[int(raw_choice) - 1]
|
|
)
|
|
if os.path.exists(chosen):
|
|
wordlist_choice = chosen
|
|
print(wordlist_choice)
|
|
elif os.path.exists(raw_choice):
|
|
wordlist_choice = raw_choice
|
|
else:
|
|
print("[!] Wordlist not found. Please enter a valid path.")
|
|
return
|
|
except ValueError:
|
|
print("Please enter a valid number.")
|
|
|
|
ctx.hcatGenerateRules(ctx.hcatHashType, ctx.hcatHashFile, rule_count, wordlist_choice)
|
|
|
|
|
|
def ngram_attack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("N-gram")
|
|
print("\n" + "=" * 60)
|
|
print("NGRAM ATTACK")
|
|
print("=" * 60)
|
|
print("Generates n-gram candidates from a corpus file via ngramX.bin.")
|
|
print("Gzip-compressed corpus files are auto-detected and decompressed.")
|
|
print("=" * 60)
|
|
|
|
corpus = ctx.select_file_with_autocomplete(
|
|
"Select corpus file (tab to autocomplete)",
|
|
base_dir=ctx.hcatWordlists,
|
|
)
|
|
if not corpus:
|
|
print("No corpus selected. Aborting ngram attack.")
|
|
return
|
|
|
|
group_size_raw = input("\nEnter n-gram group size (default 3): ").strip()
|
|
try:
|
|
group_size = int(group_size_raw) if group_size_raw else 3
|
|
except ValueError:
|
|
print("[!] Invalid group size. Using default of 3.")
|
|
group_size = 3
|
|
|
|
ctx.hcatNgramX(ctx.hcatHashType, ctx.hcatHashFile, corpus, group_size)
|
|
|
|
|
|
def permute_crack(ctx: Any) -> None:
|
|
_notify.prompt_notify_for_attack("Permute")
|
|
print("\n" + "=" * 60)
|
|
print("PERMUTATION ATTACK")
|
|
print("=" * 60)
|
|
print("Generates ALL character permutations of each word in a targeted wordlist.")
|
|
print("WARNING: Scales as N! per word. Only practical for words up to ~8 characters.")
|
|
print("Best for: short targeted wordlists (names, abbreviations, known fragments).")
|
|
print("=" * 60)
|
|
|
|
def path_completer(text, state):
|
|
base = ctx.hcatWordlists
|
|
if not text:
|
|
pattern = os.path.join(base, "*")
|
|
matches = glob.glob(pattern)
|
|
else:
|
|
text = os.path.expanduser(text)
|
|
if text.startswith(("/", "./", "../", "~")):
|
|
matches = glob.glob(text + "*")
|
|
else:
|
|
pattern = os.path.join(base, text + "*")
|
|
matches = glob.glob(pattern)
|
|
matches = [m + "/" if os.path.isdir(m) else m for m in matches]
|
|
try:
|
|
return matches[state]
|
|
except IndexError:
|
|
return None
|
|
|
|
_configure_readline(path_completer)
|
|
|
|
wordlist_path = None
|
|
while wordlist_path is None:
|
|
raw = input(
|
|
"\nEnter path to a wordlist FILE (tab to autocomplete): "
|
|
).strip()
|
|
if not raw:
|
|
continue
|
|
if not os.path.exists(raw):
|
|
print(f"[!] Path not found: {raw}")
|
|
continue
|
|
if os.path.isdir(raw):
|
|
print("[!] A directory was provided. Please enter a single wordlist file.")
|
|
continue
|
|
wordlist_path = raw
|
|
|
|
ctx.hcatPermute(ctx.hcatHashType, ctx.hcatHashFile, wordlist_path)
|
|
|
|
|
|
|
|
def combinator_submenu(ctx: Any) -> None:
|
|
items = [
|
|
("1", "Combinator Attack (2-8 wordlists)"),
|
|
("2", "YOLO Combinator Attack"),
|
|
("3", "Middle Combinator Attack"),
|
|
("4", "Thorough Combinator Attack"),
|
|
("99", "Back to Main Menu"),
|
|
]
|
|
while True:
|
|
choice = interactive_menu(items, title="\nCombinator Attacks:")
|
|
if choice is None or choice == "99":
|
|
break
|
|
elif choice == "1":
|
|
combinator_crack(ctx)
|
|
elif choice == "2":
|
|
yolo_combination(ctx)
|
|
elif choice == "3":
|
|
middle_combinator(ctx)
|
|
elif choice == "4":
|
|
thorough_combinator(ctx)
|
|
|
|
|
|
def _rule_select_file(ctx: Any, prompt: str = "Rule file: ") -> str:
|
|
"""Prompt for a rule file path with tab-autocomplete."""
|
|
import glob as _glob
|
|
|
|
def rule_completer(text: str, state: int) -> str | None:
|
|
base = ctx.rulesDirectory
|
|
if not text:
|
|
pattern = os.path.join(base, "*.rule")
|
|
else:
|
|
text = os.path.expanduser(text)
|
|
if text.startswith(("/", "./", "../", "~")):
|
|
pattern = text + "*"
|
|
else:
|
|
pattern = os.path.join(base, text + "*")
|
|
matches = _glob.glob(pattern)
|
|
try:
|
|
return matches[state]
|
|
except IndexError:
|
|
return None
|
|
|
|
_configure_readline(rule_completer)
|
|
return input(prompt).strip()
|
|
|
|
|
|
def rule_cleanup_handler(ctx: Any) -> None:
|
|
"""Clean a rule file using cleanup-rules.bin."""
|
|
print("\nClean rule file - removes invalid and duplicate rules.")
|
|
print("Reads an input rule file and writes cleaned rules to an output file.\n")
|
|
infile = _rule_select_file(ctx, "Input rule file (tab to autocomplete): ")
|
|
if not infile or not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outfile = input("Output file path: ").strip()
|
|
if not outfile:
|
|
print("[!] Output path required.")
|
|
return
|
|
print(f"\nCleaning {infile} -> {outfile}")
|
|
if ctx.rules_cleanup(infile, outfile):
|
|
print("[+] Done.")
|
|
else:
|
|
print("[!] Cleanup failed.")
|
|
|
|
|
|
def rule_optimize_handler(ctx: Any) -> None:
|
|
"""Optimize a rule file using rules_optimize.bin."""
|
|
print("\nOptimize rule file - consolidates redundant operations.")
|
|
infile = _rule_select_file(ctx, "Input rule file: ")
|
|
if not infile or not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outfile = input("Output file path: ").strip()
|
|
if not outfile:
|
|
print("[!] Output path required.")
|
|
return
|
|
print(f"\nOptimizing {infile} -> {outfile}")
|
|
if ctx.rules_optimize(infile, outfile):
|
|
print("[+] Done.")
|
|
else:
|
|
print("[!] Optimize failed.")
|
|
|
|
|
|
def rule_cleanup_and_optimize_handler(ctx: Any) -> None:
|
|
"""Clean then optimize a rule file."""
|
|
import tempfile
|
|
|
|
print("\nClean and optimize rule file (both operations in sequence).")
|
|
infile = _rule_select_file(ctx, "Input rule file: ")
|
|
if not infile or not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outfile = input("Output file path: ").strip()
|
|
if not outfile:
|
|
print("[!] Output path required.")
|
|
return
|
|
with tempfile.NamedTemporaryFile(suffix=".rule", delete=False) as tmp:
|
|
tmp_path = tmp.name
|
|
try:
|
|
print(f"\nStep 1/2: Cleaning {infile}...")
|
|
if not ctx.rules_cleanup(infile, tmp_path):
|
|
print("[!] Cleanup failed.")
|
|
return
|
|
print(f"Step 2/2: Optimizing -> {outfile}...")
|
|
if ctx.rules_optimize(tmp_path, outfile):
|
|
print("[+] Done.")
|
|
else:
|
|
print("[!] Optimize failed.")
|
|
finally:
|
|
if os.path.exists(tmp_path):
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
def rule_tools_submenu(ctx: Any) -> None:
|
|
from hate_crack.menu import interactive_menu
|
|
|
|
items = [
|
|
("1", "Clean rule file (remove invalid/duplicate rules)"),
|
|
("2", "Optimize rule file (consolidate redundant operations)"),
|
|
("3", "Clean and optimize rule file (both)"),
|
|
("99", "Back to Main Menu"),
|
|
]
|
|
while True:
|
|
choice = interactive_menu(items, title="\nRule File Tools:")
|
|
if choice is None or choice == "99":
|
|
break
|
|
elif choice == "1":
|
|
rule_cleanup_handler(ctx)
|
|
elif choice == "2":
|
|
rule_optimize_handler(ctx)
|
|
elif choice == "3":
|
|
rule_cleanup_and_optimize_handler(ctx)
|
|
|
|
|
|
def wordlist_filter_length(ctx: Any) -> None:
|
|
"""Prompt for paths and lengths, then filter wordlist by word length."""
|
|
infile = ctx.select_file_with_autocomplete(
|
|
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
|
if not outfile:
|
|
print("[!] Output path cannot be empty.")
|
|
return
|
|
min_len = int(input("[*] Minimum length: ").strip() or "0")
|
|
max_len = int(input("[*] Maximum length: ").strip() or "0")
|
|
if ctx.wordlist_filter_len(infile, outfile, min_len, max_len):
|
|
print(f"\n[*] Filtered wordlist written to: {outfile}")
|
|
else:
|
|
print("[!] Filter failed.")
|
|
|
|
|
|
def wordlist_filter_charclass_include(ctx: Any) -> None:
|
|
"""Prompt for paths and mask, then keep only words matching required char classes."""
|
|
infile = ctx.select_file_with_autocomplete(
|
|
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
|
if not outfile:
|
|
print("[!] Output path cannot be empty.")
|
|
return
|
|
print("[*] Char class mask: 1=lowercase, 2=uppercase, 4=digit, 8=symbol (additive, e.g. 3=lower+upper)")
|
|
mask = int(input("[*] Enter mask value: ").strip() or "0")
|
|
if ctx.wordlist_filter_req_include(infile, outfile, mask):
|
|
print(f"\n[*] Filtered wordlist written to: {outfile}")
|
|
else:
|
|
print("[!] Filter failed.")
|
|
|
|
|
|
def wordlist_filter_charclass_exclude(ctx: Any) -> None:
|
|
"""Prompt for paths and mask, then remove words containing excluded char classes."""
|
|
infile = ctx.select_file_with_autocomplete(
|
|
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
|
if not outfile:
|
|
print("[!] Output path cannot be empty.")
|
|
return
|
|
print("[*] Char class mask: 1=lowercase, 2=uppercase, 4=digit, 8=symbol (additive)")
|
|
mask = int(input("[*] Enter mask value: ").strip() or "0")
|
|
if ctx.wordlist_filter_req_exclude(infile, outfile, mask):
|
|
print(f"\n[*] Filtered wordlist written to: {outfile}")
|
|
else:
|
|
print("[!] Filter failed.")
|
|
|
|
|
|
def wordlist_cut_substring(ctx: Any) -> None:
|
|
"""Prompt for paths, offset, and optional length, then extract substring from each word."""
|
|
infile = ctx.select_file_with_autocomplete(
|
|
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
|
if not outfile:
|
|
print("[!] Output path cannot be empty.")
|
|
return
|
|
offset = int(input("[*] Byte offset to start from: ").strip() or "0")
|
|
raw_length = input("[*] Length (leave blank for rest of line): ").strip()
|
|
length = int(raw_length) if raw_length else None
|
|
if ctx.wordlist_cutb(infile, outfile, offset, length):
|
|
print(f"\n[*] Output written to: {outfile}")
|
|
else:
|
|
print("[!] Cut failed.")
|
|
|
|
|
|
def wordlist_split_by_length(ctx: Any) -> None:
|
|
"""Prompt for input wordlist and output directory, then split by word length."""
|
|
infile = ctx.select_file_with_autocomplete(
|
|
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
|
|
if not outdir:
|
|
print("[!] Output directory cannot be empty.")
|
|
return
|
|
os.makedirs(outdir, exist_ok=True)
|
|
if ctx.wordlist_splitlen(infile, outdir):
|
|
print(f"\n[*] Split wordlists written to: {outdir}")
|
|
else:
|
|
print("[!] Split failed.")
|
|
|
|
|
|
def wordlist_subtract_words(ctx: Any) -> None:
|
|
"""Prompt for mode then remove matching lines from a wordlist."""
|
|
print("\n[*] Subtract mode:")
|
|
print(" 1. Single remove file (rli2 - faster for one file)")
|
|
print(" 2. Multiple remove files (rli)")
|
|
mode = input("[*] Choose mode (1/2): ").strip()
|
|
|
|
if mode == "1":
|
|
infile = ctx.select_file_with_autocomplete(
|
|
"[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
remove_file = ctx.select_file_with_autocomplete(
|
|
"[*] Enter path to wordlist to subtract", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(remove_file):
|
|
print(f"[!] File not found: {remove_file}")
|
|
return
|
|
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
|
if not outfile:
|
|
print("[!] Output path cannot be empty.")
|
|
return
|
|
if ctx.wordlist_subtract_single(infile, remove_file, outfile):
|
|
print(f"\n[*] Result written to: {outfile}")
|
|
else:
|
|
print("[!] Subtraction failed.")
|
|
elif mode == "2":
|
|
infile = ctx.select_file_with_autocomplete(
|
|
"[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
|
if not outfile:
|
|
print("[!] Output path cannot be empty.")
|
|
return
|
|
raw = ctx.select_file_with_autocomplete(
|
|
"[*] Enter remove file paths", allow_multiple=True, base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
remove_files = [r.strip() for r in raw.split(",") if r.strip()]
|
|
if not remove_files:
|
|
print("[!] No remove files provided.")
|
|
return
|
|
if ctx.wordlist_subtract(infile, outfile, *remove_files):
|
|
print(f"\n[*] Deduplicated wordlist written to: {outfile}")
|
|
else:
|
|
print("[!] Subtraction failed.")
|
|
else:
|
|
print("[!] Invalid mode.")
|
|
|
|
|
|
def wordlist_shard(ctx: Any) -> None:
|
|
"""Prompt for input/output base path and shard count, then write all N part files."""
|
|
infile = ctx.select_file_with_autocomplete(
|
|
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
|
|
).strip()
|
|
if not os.path.isfile(infile):
|
|
print(f"[!] File not found: {infile}")
|
|
return
|
|
outbase = ctx.select_file_with_autocomplete(
|
|
"[*] Enter output base path (part numbers are appended, e.g. wl.001)"
|
|
).strip()
|
|
if not outbase:
|
|
print("[!] Output path cannot be empty.")
|
|
return
|
|
mod = int(input("[*] Shard count (e.g. 4 to split into 4 parts): ").strip() or "0")
|
|
if mod < 2:
|
|
print("[!] Shard count must be at least 2.")
|
|
return
|
|
width = max(3, len(str(mod)))
|
|
written = []
|
|
for offset in range(mod):
|
|
outfile = f"{outbase}.{offset + 1:0{width}d}"
|
|
if ctx.wordlist_gate(infile, outfile, mod, offset):
|
|
written.append(outfile)
|
|
else:
|
|
print(f"[!] Shard failed at part {offset + 1}: {outfile}")
|
|
return
|
|
print(f"\n[*] Wrote {len(written)} shard(s):")
|
|
for path in written:
|
|
print(f" {path}")
|
|
|
|
|
|
def wordlist_optimize(ctx: Any) -> None:
|
|
"""Prompt for input wordlists and output directory, then optimize."""
|
|
raw = ctx.select_file_with_autocomplete(
|
|
"\n[*] Enter input wordlist paths (comma-separated files or directories)",
|
|
base_dir=ctx.hcatWordlists,
|
|
).strip()
|
|
raw_entries = [p.strip() for p in raw.split(",") if p.strip()]
|
|
if not raw_entries:
|
|
print("[!] No input wordlists provided.")
|
|
return
|
|
inputs: list[str] = []
|
|
not_found: list[str] = []
|
|
for entry in raw_entries:
|
|
if os.path.isfile(entry):
|
|
inputs.append(entry)
|
|
elif os.path.isdir(entry):
|
|
files = [os.path.join(entry, f) for f in ctx.list_wordlist_files(entry)]
|
|
if not files:
|
|
print(f"[!] No wordlist files found in: {entry}")
|
|
return
|
|
inputs.extend(files)
|
|
else:
|
|
not_found.append(entry)
|
|
if not_found:
|
|
print("[!] Not found (not a file or directory):")
|
|
for p in not_found:
|
|
print(f" {p}")
|
|
return
|
|
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
|
|
if not outdir:
|
|
print("[!] Output directory cannot be empty.")
|
|
return
|
|
if ctx.wordlist_optimize(inputs, outdir):
|
|
print(f"\n[*] Optimized wordlists written to: {outdir}")
|
|
else:
|
|
print("[!] Optimization failed.")
|
|
|
|
|
|
def wordlist_tools_submenu(ctx: Any) -> None:
|
|
"""Display the Wordlist Tools submenu and dispatch to the selected handler."""
|
|
items = [
|
|
("1", "Filter by Length"),
|
|
("2", "Require Character Classes"),
|
|
("3", "Exclude Character Classes"),
|
|
("4", "Extract Substring"),
|
|
("5", "Split by Length"),
|
|
("6", "Subtract Wordlist"),
|
|
("7", "Shard Wordlist"),
|
|
("8", "Optimize Wordlists"),
|
|
("99", "Back to Main Menu"),
|
|
]
|
|
while True:
|
|
choice = interactive_menu(items, title="\nWordlist Tools:")
|
|
if choice is None or choice == "99":
|
|
break
|
|
elif choice == "1":
|
|
wordlist_filter_length(ctx)
|
|
elif choice == "2":
|
|
wordlist_filter_charclass_include(ctx)
|
|
elif choice == "3":
|
|
wordlist_filter_charclass_exclude(ctx)
|
|
elif choice == "4":
|
|
wordlist_cut_substring(ctx)
|
|
elif choice == "5":
|
|
wordlist_split_by_length(ctx)
|
|
elif choice == "6":
|
|
wordlist_subtract_words(ctx)
|
|
elif choice == "7":
|
|
wordlist_shard(ctx)
|
|
elif choice == "8":
|
|
wordlist_optimize(ctx)
|