feat: add ad-hoc mask attack, markov brute force, and combinator sub-menu

- Add three hashcat wrapper functions: hcatAdHocMask, hcatMarkovTrain, hcatMarkovBruteForce
- Add corresponding attack handlers in attacks.py with OMEN-style training flow
- Consolidate 4 combinator attacks (keys 10/11/12) into interactive sub-menu (key 6)
- Add key 17 for ad-hoc mask attack and key 18 for markov brute force
- Update both main.py and hate_crack.py menu systems
- Add comprehensive test coverage for new handlers and wrappers
- Training source picker supports cracked passwords or any wordlist
This commit is contained in:
Justin Bollinger
2026-03-18 19:00:40 -04:00
parent eb3f484d2b
commit 428bb7cc54
4 changed files with 404 additions and 12 deletions
+3 -4
View File
@@ -78,17 +78,16 @@ def get_main_menu_options():
"3": _attacks.brute_force_crack,
"4": _attacks.top_mask_crack,
"5": _attacks.fingerprint_crack,
"6": _attacks.combinator_crack,
"6": _attacks.combinator_submenu,
"7": _attacks.hybrid_crack,
"8": _attacks.pathwell_crack,
"9": _attacks.prince_attack,
"10": _attacks.yolo_combination,
"11": _attacks.middle_combinator,
"12": _attacks.thorough_combinator,
"13": _attacks.bandrel_method,
"14": _attacks.loopback_attack,
"15": _attacks.ollama_attack,
"16": _attacks.omen_attack,
"17": _attacks.adhoc_mask_crack,
"18": _attacks.markov_brute_force,
"90": download_hashmob_rules,
"91": weakpass_wordlist_menu,
"92": download_hashmob_wordlists,
+120
View File
@@ -522,3 +522,123 @@ def omen_attack(ctx: Any) -> None:
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:
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:
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 combinator_submenu(ctx: Any) -> None:
from hate_crack.menu import interactive_menu
items = [
("1", "Combinator Attack"),
("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)
+100 -8
View File
@@ -102,6 +102,8 @@ DEFAULT_OPTIMIZED_ATTACKS = frozenset(
"hcatBruteForce",
"hcatTopMask",
"hcatPathwellBruteForce",
"hcatAdHocMask",
"hcatMarkovBruteForce",
}
)
@@ -2042,6 +2044,86 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile):
hcatProcess.kill()
def hcatAdHocMask(hcatHashType, hcatHashFile, mask, custom_charsets=""):
global hcatProcess
cmd = [
hcatBin,
"-m",
hcatHashType,
hcatHashFile,
"--session",
generate_session_id(),
"-o",
f"{hcatHashFile}.out",
"-a",
"3",
]
if custom_charsets:
cmd.extend(shlex.split(custom_charsets))
cmd.append(mask)
if _should_use_optimized_kernel("hcatAdHocMask"):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
def hcatMarkovTrain(source_file, hcatHashFile):
global hcatProcess
hcstat2gen_bin = os.path.join(hate_path, "hashcat-utils", "bin", hcatHcstat2genBin)
hcstat2_path = f"{hcatHashFile}.hcstat2"
print(f"[*] Generating markov table -> {hcstat2_path}")
with open(source_file, "rb") as stdin_f, open(hcstat2_path, "wb") as stdout_f:
hcatProcess = subprocess.Popen(
[hcstat2gen_bin], stdin=stdin_f, stdout=stdout_f
)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
return False
return os.path.isfile(hcstat2_path) and os.path.getsize(hcstat2_path) > 0
def hcatMarkovBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
global hcatProcess
hcstat2_path = f"{hcatHashFile}.hcstat2"
cmd = [
hcatBin,
"-m",
hcatHashType,
hcatHashFile,
"--session",
generate_session_id(),
"-o",
f"{hcatHashFile}.out",
"--markov-hcstat2",
hcstat2_path,
"--increment",
f"--increment-min={hcatMinLen}",
f"--increment-max={hcatMaxLen}",
"-a",
"3",
"?a?a?a?a?a?a?a?a?a?a?a?a?a?a",
]
if _should_use_optimized_kernel("hcatMarkovBruteForce"):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
# PRINCE Attack
def hcatPrince(hcatHashType, hcatHashFile):
global hcatProcess
@@ -3169,6 +3251,18 @@ def middle_combinator():
return _attacks.middle_combinator(_attack_ctx())
def combinator_submenu():
return _attacks.combinator_submenu(_attack_ctx())
def adhoc_mask_crack():
return _attacks.adhoc_mask_crack(_attack_ctx())
def markov_brute_force():
return _attacks.markov_brute_force(_attack_ctx())
def bandrel_method():
return _attacks.bandrel_method(_attack_ctx())
@@ -3403,17 +3497,16 @@ def get_main_menu_items():
("3", "Brute Force Attack"),
("4", "Top Mask Attack"),
("5", "Fingerprint Attack"),
("6", "Combinator Attack"),
("6", "Combinator Attacks"),
("7", "Hybrid Attack"),
("8", "Pathwell Top 100 Mask Brute Force Crack"),
("9", "PRINCE Attack"),
("10", "YOLO Combinator Attack"),
("11", "Middle Combinator Attack"),
("12", "Thorough Combinator Attack"),
("13", "Bandrel Methodology"),
("14", "Loopback Attack"),
("15", "LLM Attack"),
("16", "OMEN Attack"),
("17", "Ad-hoc Mask Attack"),
("18", "Markov Brute Force Attack"),
("90", "Download rules from Hashmob.net"),
("91", "Analyze Hashcat Rules"),
("92", "Download wordlists from Hashmob.net"),
@@ -3441,17 +3534,16 @@ def get_main_menu_options():
"3": brute_force_crack,
"4": top_mask_crack,
"5": fingerprint_crack,
"6": combinator_crack,
"6": combinator_submenu,
"7": hybrid_crack,
"8": pathwell_crack,
"9": prince_attack,
"10": yolo_combination,
"11": middle_combinator,
"12": thorough_combinator,
"13": bandrel_method,
"14": loopback_attack,
"15": ollama_attack,
"16": omen_attack,
"17": adhoc_mask_crack,
"18": markov_brute_force,
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
"91": analyze_rules,
"92": download_hashmob_wordlists,
+181
View File
@@ -0,0 +1,181 @@
"""Tests for ad-hoc mask attack, markov brute force, and combinator submenu."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
def _make_ctx(hash_type: str = "1000", hash_file: str = "/tmp/hashes.txt") -> MagicMock:
ctx = MagicMock()
ctx.hcatHashType = hash_type
ctx.hcatHashFile = hash_file
ctx.hcatWordlists = "/tmp/wordlists"
return ctx
class TestAdHocMaskHandler:
"""Test the adhoc_mask_crack handler for user input and flow."""
def test_basic_mask(self) -> None:
"""User enters mask, no custom charsets."""
from hate_crack.attacks import adhoc_mask_crack
ctx = _make_ctx()
with patch("builtins.input", side_effect=["?l?l?l?l", ""]):
adhoc_mask_crack(ctx)
ctx.hcatAdHocMask.assert_called_once_with("1000", "/tmp/hashes.txt", "?l?l?l?l", "")
def test_empty_mask_aborts(self) -> None:
"""Empty mask string causes early return."""
from hate_crack.attacks import adhoc_mask_crack
ctx = _make_ctx()
with patch("builtins.input", return_value=""):
adhoc_mask_crack(ctx)
ctx.hcatAdHocMask.assert_not_called()
def test_custom_charset_passed(self) -> None:
"""User enters custom charset -1."""
from hate_crack.attacks import adhoc_mask_crack
ctx = _make_ctx()
with patch("builtins.input", side_effect=["?1?1?1?1", "abc", ""]):
adhoc_mask_crack(ctx)
ctx.hcatAdHocMask.assert_called_once()
call_args = ctx.hcatAdHocMask.call_args
assert call_args[0][2] == "?1?1?1?1"
assert "-1" in call_args[0][3]
assert "abc" in call_args[0][3]
class TestHcatAdHocMask:
"""Test the hcatAdHocMask wrapper function."""
def test_mask_attack_command(self, tmp_path: Path) -> None:
"""Verify mask attack command structure."""
from hate_crack import main
hash_file = str(tmp_path / "hashes.txt")
with patch("subprocess.Popen") as mock_popen:
mock_process = MagicMock()
mock_process.wait.return_value = None
mock_popen.return_value = mock_process
main.hcatAdHocMask("1000", hash_file, "?l?l?d?d", "")
call_args = mock_popen.call_args[0][0]
assert "-m" in call_args
assert "1000" in call_args
assert hash_file in call_args
assert "-a" in call_args
assert "3" in call_args
assert "?l?l?d?d" in call_args
class TestMarkovBruteForceHandler:
"""Test markov_brute_force handler logic with table reuse options."""
def test_use_existing_table(self, tmp_path: Path) -> None:
"""User chooses to use existing .hcstat2 table."""
from hate_crack.attacks import markov_brute_force
ctx = _make_ctx()
hash_file = str(tmp_path / "hashes.txt")
ctx.hcatHashFile = hash_file
hcstat2_path = f"{hash_file}.hcstat2"
Path(hcstat2_path).touch()
with patch("builtins.input", side_effect=["1", "1", "7"]):
markov_brute_force(ctx)
ctx.hcatMarkovTrain.assert_not_called()
ctx.hcatMarkovBruteForce.assert_called_once()
def test_no_table_requires_training(self, tmp_path: Path) -> None:
"""No table exists, training is triggered."""
from hate_crack.attacks import markov_brute_force
ctx = _make_ctx()
hash_file = str(tmp_path / "hashes.txt")
ctx.hcatHashFile = hash_file
ctx.hcatMarkovTrain.return_value = True
ctx.list_wordlist_files.return_value = ["test.txt"]
with patch("builtins.input", side_effect=["1", "1", "6"]):
markov_brute_force(ctx)
ctx.hcatMarkovTrain.assert_called_once()
ctx.hcatMarkovBruteForce.assert_called_once()
def test_training_failure_aborts(self, tmp_path: Path) -> None:
"""Training failure aborts without calling brute force."""
from hate_crack.attacks import markov_brute_force
ctx = _make_ctx()
hash_file = str(tmp_path / "hashes.txt")
ctx.hcatHashFile = hash_file
ctx.hcatMarkovTrain.return_value = False
ctx.list_wordlist_files.return_value = ["test.txt"]
with patch("builtins.input", side_effect=["1", "1"]):
markov_brute_force(ctx)
ctx.hcatMarkovTrain.assert_called_once()
ctx.hcatMarkovBruteForce.assert_not_called()
class TestHcatMarkovBruteForce:
"""Test hcatMarkovBruteForce wrapper function."""
def test_markov_flags_in_cmd(self, tmp_path: Path) -> None:
"""Verify markov attack command includes table and increment flags."""
from hate_crack import main
hash_file = str(tmp_path / "hashes.txt")
hcstat2_path = f"{hash_file}.hcstat2"
Path(hcstat2_path).touch()
with patch("subprocess.Popen") as mock_popen:
mock_process = MagicMock()
mock_process.wait.return_value = None
mock_popen.return_value = mock_process
main.hcatMarkovBruteForce("1000", hash_file, 1, 7)
call_args = mock_popen.call_args[0][0]
assert "--markov-hcstat2" in call_args
assert hcstat2_path in call_args
assert "--increment" in call_args
assert "--increment-min=1" in call_args
assert "--increment-max=7" in call_args
class TestHcatMarkovTrain:
"""Test hcatMarkovTrain wrapper function."""
def test_success_with_output(self, tmp_path: Path) -> None:
"""Training succeeds when output file is non-empty."""
from hate_crack import main
source_file = str(tmp_path / "source.txt")
source_file_path = Path(source_file)
source_file_path.write_text("password1\npassword2\n")
hash_file = str(tmp_path / "hashes.txt")
hcstat2_path = f"{hash_file}.hcstat2"
with patch("subprocess.Popen") as mock_popen:
mock_process = MagicMock()
mock_process.wait.return_value = None
mock_popen.return_value = mock_process
with patch("os.path.isfile", return_value=True):
with patch("os.path.getsize", return_value=1024):
result = main.hcatMarkovTrain(source_file, hash_file)
assert result is True