mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-06-12 19:11:46 -07:00
f0e123ce93
Pushover notifications were silently dropped for Quick Crack, Loopback,
Combinator, PRINCE-LING and N-gram because the handlers prompted under
one name (e.g. "Quick Crack") while the underlying hashcat wrapper
fired with a different label ("Quick Dictionary"). _should_fire keyed
the per-run consent on the prompt name, so the consent check at fire
time always missed.
Threaded the prompt name down to _run_hcat_cmd by:
- Adding attack_name kwargs to hcatQuickDictionary and hcatPrince, the
two wrappers shared by multiple user-facing handlers.
- Relabelling hcatCombination/Combinator3/CombinatorX (all reach the
user via combinator_crack which prompts "Combinator") and hcatNgramX
(single caller: ngram_attack prompting "N-gram").
- Updating quick_crack, loopback_attack, and hcatPrinceLing to pass the
matching attack_name override.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
304 lines
13 KiB
Python
304 lines
13 KiB
Python
"""Regression tests for issue #110: pushover notifications were silently
|
|
dropped because the name passed to ``prompt_notify_for_attack`` (and
|
|
cached in ``_run_consent``) differed from the ``attack_name`` later
|
|
passed to ``_run_hcat_cmd``.
|
|
|
|
Each test below pins the contract: the same string the user consented
|
|
to under is the string that ``_run_hcat_cmd`` receives.
|
|
"""
|
|
import os
|
|
from contextlib import ExitStack
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def main_module(hc_module):
|
|
return hc_module._main
|
|
|
|
|
|
def _common_patches(main_module):
|
|
return [
|
|
patch.object(main_module, "hcatBin", "hashcat"),
|
|
patch.object(main_module, "hcatTuning", ""),
|
|
patch.object(main_module, "hcatPotfilePath", ""),
|
|
patch.object(main_module, "generate_session_id", return_value="test_session"),
|
|
]
|
|
|
|
|
|
def _enter_all(stack: ExitStack, ctxmgrs):
|
|
return [stack.enter_context(cm) for cm in ctxmgrs]
|
|
|
|
|
|
class TestQuickDictionaryAttackName:
|
|
"""``hcatQuickDictionary`` is shared by Quick Crack and Loopback.
|
|
Both must surface their own prompt name to ``_run_hcat_cmd``.
|
|
"""
|
|
|
|
def test_default_attack_name_preserved(self, main_module, tmp_path: Path) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
wordlist = str(tmp_path / "words.txt")
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
stack.enter_context(
|
|
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c)
|
|
)
|
|
stack.enter_context(patch("hate_crack.main._debug_cmd"))
|
|
_enter_all(stack, _common_patches(main_module))
|
|
main_module.hcatQuickDictionary("1000", hash_file, "", wordlist)
|
|
assert run.call_args.kwargs["attack_name"] == "Quick Dictionary"
|
|
|
|
def test_quick_crack_override(self, main_module, tmp_path: Path) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
wordlist = str(tmp_path / "words.txt")
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
stack.enter_context(
|
|
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c)
|
|
)
|
|
stack.enter_context(patch("hate_crack.main._debug_cmd"))
|
|
_enter_all(stack, _common_patches(main_module))
|
|
main_module.hcatQuickDictionary(
|
|
"1000", hash_file, "", wordlist, attack_name="Quick Crack"
|
|
)
|
|
assert run.call_args.kwargs["attack_name"] == "Quick Crack"
|
|
|
|
def test_loopback_override(self, main_module, tmp_path: Path) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
wordlist = str(tmp_path / "words.txt")
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
stack.enter_context(
|
|
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c)
|
|
)
|
|
stack.enter_context(patch("hate_crack.main._debug_cmd"))
|
|
_enter_all(stack, _common_patches(main_module))
|
|
main_module.hcatQuickDictionary(
|
|
"1000",
|
|
hash_file,
|
|
"",
|
|
wordlist,
|
|
loopback=True,
|
|
attack_name="Loopback",
|
|
)
|
|
assert run.call_args.kwargs["attack_name"] == "Loopback"
|
|
|
|
|
|
class TestPrinceAttackName:
|
|
"""``hcatPrince`` is shared by PRINCE (direct) and PRINCE-LING
|
|
(delegated via ``hcatPrinceLing``). Both must surface their own
|
|
name.
|
|
"""
|
|
|
|
def _stub_prince_env(self, main_module, tmp_path: Path):
|
|
prince_base = tmp_path / "prince_base.txt"
|
|
prince_base.write_text("password\n")
|
|
prince_dir = tmp_path / "princeprocessor"
|
|
prince_dir.mkdir()
|
|
(prince_dir / "pp64.bin").touch()
|
|
return [
|
|
patch.object(main_module, "hate_path", str(tmp_path)),
|
|
patch.object(main_module, "hcatPrinceBaseList", [str(prince_base)]),
|
|
]
|
|
|
|
def test_default_attack_name_is_prince(self, main_module, tmp_path: Path) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
popen = stack.enter_context(patch("hate_crack.main.subprocess.Popen"))
|
|
popen.return_value = MagicMock(stdout=MagicMock(), wait=MagicMock())
|
|
_enter_all(stack, _common_patches(main_module))
|
|
_enter_all(stack, self._stub_prince_env(main_module, tmp_path))
|
|
main_module.hcatPrince("1000", hash_file)
|
|
assert run.call_args.kwargs["attack_name"] == "PRINCE"
|
|
|
|
def test_prince_ling_override(self, main_module, tmp_path: Path) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
popen = stack.enter_context(patch("hate_crack.main.subprocess.Popen"))
|
|
popen.return_value = MagicMock(stdout=MagicMock(), wait=MagicMock())
|
|
_enter_all(stack, _common_patches(main_module))
|
|
_enter_all(stack, self._stub_prince_env(main_module, tmp_path))
|
|
main_module.hcatPrince("1000", hash_file, attack_name="PRINCE-LING")
|
|
assert run.call_args.kwargs["attack_name"] == "PRINCE-LING"
|
|
|
|
|
|
class TestSingleCallerWrapperNames:
|
|
"""Wrappers that have a single user-facing handler should pass the
|
|
name the user saw at the prompt, not the internal function-style
|
|
label.
|
|
"""
|
|
|
|
def test_ngram_x_uses_ngram_label(self, main_module, tmp_path: Path) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
corpus = tmp_path / "corpus.txt"
|
|
corpus.write_text("word\n")
|
|
ngram_dir = tmp_path / "hashcat-utils" / "bin"
|
|
ngram_dir.mkdir(parents=True)
|
|
(ngram_dir / "ngramX.bin").touch()
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
popen = stack.enter_context(patch("hate_crack.main.subprocess.Popen"))
|
|
popen.return_value = MagicMock(stdout=MagicMock(), wait=MagicMock())
|
|
stack.enter_context(patch.object(main_module, "hate_path", str(tmp_path)))
|
|
_enter_all(stack, _common_patches(main_module))
|
|
main_module.hcatNgramX("1000", hash_file, str(corpus), 3)
|
|
assert run.call_args.kwargs["attack_name"] == "N-gram"
|
|
|
|
def test_combination_uses_combinator_label(
|
|
self, main_module, tmp_path: Path
|
|
) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
wl1 = tmp_path / "w1.txt"
|
|
wl2 = tmp_path / "w2.txt"
|
|
wl1.write_text("a\n")
|
|
wl2.write_text("b\n")
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
stack.enter_context(patch.object(main_module, "hcatWordlists", str(tmp_path)))
|
|
stack.enter_context(patch.object(main_module, "lineCount", return_value=0))
|
|
stack.enter_context(
|
|
patch.object(main_module, "hcatHashCracked", 0, create=True)
|
|
)
|
|
_enter_all(stack, _common_patches(main_module))
|
|
main_module.hcatCombination(
|
|
"1000", hash_file, wordlists=[str(wl1), str(wl2)]
|
|
)
|
|
assert run.call_args.kwargs["attack_name"] == "Combinator"
|
|
|
|
def test_combinator3_uses_combinator_label(
|
|
self, main_module, tmp_path: Path
|
|
) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
wls = []
|
|
for i in range(3):
|
|
p = tmp_path / f"w{i}.txt"
|
|
p.write_text(f"x{i}\n")
|
|
wls.append(str(p))
|
|
combinator3 = tmp_path / "hashcat-utils" / "bin" / "combinator3.bin"
|
|
combinator3.parent.mkdir(parents=True)
|
|
combinator3.touch()
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
popen = stack.enter_context(patch("hate_crack.main.subprocess.Popen"))
|
|
popen.return_value = MagicMock(stdout=MagicMock(), wait=MagicMock())
|
|
stack.enter_context(patch.object(main_module, "hate_path", str(tmp_path)))
|
|
stack.enter_context(patch.object(main_module, "lineCount", return_value=0))
|
|
stack.enter_context(
|
|
patch.object(main_module, "hcatHashCracked", 0, create=True)
|
|
)
|
|
_enter_all(stack, _common_patches(main_module))
|
|
main_module.hcatCombinator3("1000", hash_file, wls)
|
|
assert run.call_args.kwargs["attack_name"] == "Combinator"
|
|
|
|
def test_combinatorX_uses_combinator_label(
|
|
self, main_module, tmp_path: Path
|
|
) -> None:
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
wls = []
|
|
for i in range(2):
|
|
p = tmp_path / f"w{i}.txt"
|
|
p.write_text(f"x{i}\n")
|
|
wls.append(str(p))
|
|
combinatorX = tmp_path / "hashcat-utils" / "bin" / "combinatorX.bin"
|
|
combinatorX.parent.mkdir(parents=True)
|
|
combinatorX.touch()
|
|
with ExitStack() as stack:
|
|
run = stack.enter_context(patch("hate_crack.main._run_hcat_cmd"))
|
|
popen = stack.enter_context(patch("hate_crack.main.subprocess.Popen"))
|
|
popen.return_value = MagicMock(stdout=MagicMock(), wait=MagicMock())
|
|
stack.enter_context(patch.object(main_module, "hate_path", str(tmp_path)))
|
|
stack.enter_context(patch.object(main_module, "lineCount", return_value=0))
|
|
stack.enter_context(
|
|
patch.object(main_module, "hcatHashCracked", 0, create=True)
|
|
)
|
|
_enter_all(stack, _common_patches(main_module))
|
|
main_module.hcatCombinatorX("1000", hash_file, wls)
|
|
assert run.call_args.kwargs["attack_name"] == "Combinator"
|
|
|
|
|
|
class TestHandlersPassThroughPromptName:
|
|
"""High-level: the attack-handler functions in ``attacks.py`` must
|
|
pass the same name they prompted with down to the wrapper, so that
|
|
consent set by the prompt aligns with the consent check inside
|
|
``_should_fire``.
|
|
"""
|
|
|
|
def _make_ctx(self, tmp_path: Path) -> MagicMock:
|
|
ctx = MagicMock()
|
|
ctx.hcatHashType = "1000"
|
|
ctx.hcatHashFile = str(tmp_path / "hashes.txt")
|
|
ctx.hcatWordlists = str(tmp_path / "wordlists")
|
|
ctx.rulesDirectory = str(tmp_path / "rules")
|
|
return ctx
|
|
|
|
def test_quick_crack_handler_passes_quick_crack_label(
|
|
self, tmp_path: Path
|
|
) -> None:
|
|
from hate_crack.attacks import quick_crack
|
|
|
|
ctx = self._make_ctx(tmp_path)
|
|
rules_dir = tmp_path / "rules"
|
|
rules_dir.mkdir()
|
|
(rules_dir / "best66.rule").write_text("")
|
|
wordlist_dir = tmp_path / "wordlists"
|
|
wordlist_dir.mkdir(parents=True, exist_ok=True)
|
|
wordlist = wordlist_dir / "rockyou.txt"
|
|
wordlist.write_text("password\n")
|
|
ctx.hcatOptimizedWordlists = str(wordlist_dir)
|
|
ctx.list_wordlist_files.return_value = ["rockyou.txt"]
|
|
with patch("builtins.input", side_effect=[str(wordlist), "1"]):
|
|
quick_crack(ctx)
|
|
ctx.hcatQuickDictionary.assert_called_once()
|
|
assert (
|
|
ctx.hcatQuickDictionary.call_args.kwargs.get("attack_name")
|
|
== "Quick Crack"
|
|
)
|
|
|
|
def test_loopback_handler_passes_loopback_label(self, tmp_path: Path) -> None:
|
|
from hate_crack.attacks import loopback_attack
|
|
|
|
ctx = self._make_ctx(tmp_path)
|
|
rules_dir = tmp_path / "rules"
|
|
rules_dir.mkdir()
|
|
(rules_dir / "best66.rule").write_text("")
|
|
with patch("builtins.input", return_value="1"):
|
|
loopback_attack(ctx)
|
|
ctx.hcatQuickDictionary.assert_called_once()
|
|
kw = ctx.hcatQuickDictionary.call_args.kwargs
|
|
assert kw.get("attack_name") == "Loopback"
|
|
assert kw.get("loopback") is True
|
|
|
|
def test_prince_ling_hcatPrinceLing_passes_prince_ling_label(
|
|
self, main_module, tmp_path: Path
|
|
) -> None:
|
|
"""``hcatPrinceLing`` delegates to ``hcatPrince``. The delegated
|
|
call must carry ``attack_name="PRINCE-LING"`` so per-run consent
|
|
keyed on "PRINCE-LING" actually matches.
|
|
"""
|
|
hash_file = str(tmp_path / "hashes.txt")
|
|
pcfg_root = tmp_path / "pcfg_cracker"
|
|
(pcfg_root / "Rules" / "DEFAULT").mkdir(parents=True)
|
|
(pcfg_root / "prince_ling.py").write_text("# stub")
|
|
cache_dir = tmp_path / "optimized"
|
|
cache_dir.mkdir(parents=True)
|
|
cache_path = cache_dir / "pcfg_prince_ling_DEFAULT.txt"
|
|
cache_path.write_text("password\n")
|
|
# Make ruleset_dir older than cache so no regen runs.
|
|
os.utime(pcfg_root / "Rules" / "DEFAULT", (0, 0))
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch.object(main_module, "hate_path", str(tmp_path)))
|
|
stack.enter_context(
|
|
patch.object(
|
|
main_module, "hcatOptimizedWordlists", str(cache_dir)
|
|
)
|
|
)
|
|
stack.enter_context(patch.object(main_module, "pcfgRuleset", "DEFAULT"))
|
|
prince = stack.enter_context(patch.object(main_module, "hcatPrince"))
|
|
main_module.hcatPrinceLing("1000", hash_file)
|
|
prince.assert_called_once()
|
|
assert prince.call_args.kwargs.get("attack_name") == "PRINCE-LING"
|