mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-06-22 22:56:34 -07:00
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:
+3
-4
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user