From 60404609192a45468ce5151158a891209af16dcd Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 26 May 2026 14:57:11 -0400 Subject: [PATCH] fix(fingerprint): skip hashcat when expanded wordlist is empty When no hashes have been cracked yet, the fingerprint attack's expander pipeline produces an empty {hash}.expanded file. Previously the function would still launch a hashcat combinator session against two empty wordlists (and, when invoked via the menu, six more hashcat sessions from the secondary hybrid pass) - wasting cycles and creating confusing empty intermediate files. Add an early-exit guard after the expander pipeline: if .expanded is empty, print an informative skip message and break out of the loop before invoking hashcat or hcatHybrid. Mirrors the existing "if hcatNewPasswords > 0" guard pattern in hcatRecycle. Co-Authored-By: Claude Opus 4.7 (1M context) --- hate_crack/main.py | 6 ++ tests/test_fingerprint_expander_and_hybrid.py | 83 ++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/hate_crack/main.py b/hate_crack/main.py index e06e7ca..19cba8c 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -1553,6 +1553,12 @@ def hcatFingerprint( print("Killing PID {0}...".format(str(sort_proc.pid))) sort_proc.kill() expander_proc.kill() + if lineCount(f"{hcatHashFile}.expanded") == 0: + print( + "[!] Skipping Fingerprint Attack: no candidates to expand " + "(no cracked passwords yet)." + ) + break fingerprint_cmd = [ hcatBin, "-m", diff --git a/tests/test_fingerprint_expander_and_hybrid.py b/tests/test_fingerprint_expander_and_hybrid.py index e1b2bd0..5ca1687 100644 --- a/tests/test_fingerprint_expander_and_hybrid.py +++ b/tests/test_fingerprint_expander_and_hybrid.py @@ -41,10 +41,11 @@ def test_hcatFingerprint_uses_selected_expander_and_calls_hybrid(monkeypatch, tm out_path = tmp_path / "hashes.txt.out" out_path.write_text("deadbeef:Accordbookkeeping2025!:x\n") - # Make the loop run exactly one iteration. - # Calls: before-loop(1), end-of-iteration(1) == before → break, post-loop(1). - counts = iter([1, 1, 1]) - monkeypatch.setattr(hc_main, "lineCount", lambda _p: next(counts)) + # Constant lineCount makes the while-loop terminate after one iteration + # (crackedAfter == crackedBefore) and keeps the empty-.expanded guard + # quiet (1 != 0). Avoids coupling to the exact number of lineCount + # call sites inside hcatFingerprint and _run_hcat_cmd. + monkeypatch.setattr(hc_main, "lineCount", lambda _p: 1) monkeypatch.setattr(hc_main, "hcatHashCracked", 0) # Avoid any filesystem/executable checks in unit test. @@ -102,3 +103,77 @@ def test_hcatFingerprint_uses_selected_expander_and_calls_hybrid(monkeypatch, tm assert seen["hybrid_calls"] == [ ("1000", str(hashfile), [f"{hashfile}.expanded"]), ] + + +def test_hcatFingerprint_skips_hashcat_and_hybrid_when_expanded_is_empty( + monkeypatch, tmp_path +): + """If the expander pipeline yields an empty {hash}.expanded file (e.g. + nothing has been cracked yet), hcatFingerprint must NOT invoke hashcat + and must NOT call the secondary hybrid attack. Otherwise we waste a + session running combinator against two empty wordlists.""" + monkeypatch.setenv("HATE_CRACK_SKIP_INIT", "1") + + import hate_crack.main as hc_main + + importlib.reload(hc_main) + + hashfile = tmp_path / "hashes.txt" + # Empty .out simulates "no cracks yet" — the documented trigger for the bug. + out_path = tmp_path / "hashes.txt.out" + out_path.write_text("") + + monkeypatch.setattr(hc_main, "hcatHashCracked", 0) + monkeypatch.setattr(hc_main, "ensure_binary", lambda binary_path, **_k: binary_path) + + seen = {"popen_args": [], "hybrid_calls": []} + + def fake_hybrid(hash_type, hash_file, wordlists=None): + seen["hybrid_calls"].append((hash_type, hash_file, wordlists)) + + monkeypatch.setattr(hc_main, "hcatHybrid", fake_hybrid) + + class FakePopen: + def __init__(self, args, stdin=None, stdout=None, text=False, **_kwargs): + self.args = args + self.pid = 123 + self.stdout = None + seen["popen_args"].append(args) + + cmd0 = args[0] + if cmd0 == "sort": + data = stdin.read() if stdin is not None else b"" + lines = sorted(set(data.splitlines())) + for ln in lines: + stdout.write(ln + b"\n") + stdout.flush() + elif isinstance(cmd0, str) and "expander" in cmd0: + # Identity passthrough of empty stdin → empty stdout. + data = stdin.read() if stdin is not None else b"" + self.stdout = io.BytesIO(data) + else: + # hashcat invocation: must never reach here in this test. + pass + + def wait(self, timeout=None): + return 0 + + def kill(self): + return None + + monkeypatch.setattr(hc_main.subprocess, "Popen", FakePopen) + monkeypatch.setattr(hc_main, "hcatHashFile", str(hashfile), raising=False) + + hc_main.hcatFingerprint( + "1000", str(hashfile), expander_len=7, run_hybrid_on_expanded=True + ) + + # No hashcat invocation: identify by the -a flag, which only the hashcat + # command carries (expander has 1 arg; sort uses -u). + hashcat_invocations = [args for args in seen["popen_args"] if "-a" in args] + assert hashcat_invocations == [], ( + f"hashcat was invoked with empty .expanded: {hashcat_invocations}" + ) + + # No secondary hybrid pass on an empty wordlist. + assert seen["hybrid_calls"] == []