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) <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-05-26 14:57:11 -04:00
co-authored by Claude Opus 4.7
parent 5fc54f4574
commit 6040460919
2 changed files with 85 additions and 4 deletions
+6
View File
@@ -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",
+79 -4
View File
@@ -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"] == []