updated loopback logic and added testing

This commit is contained in:
Justin Bollinger
2026-02-06 14:36:15 -05:00
parent 721628ca9a
commit 09ac410ace
5 changed files with 330 additions and 25 deletions
+1
View File
@@ -82,6 +82,7 @@ def get_main_menu_options():
"11": _attacks.middle_combinator,
"12": _attacks.thorough_combinator,
"13": _attacks.bandrel_method,
"14": _attacks.loopback_attack,
"90": download_hashmob_rules,
"91": weakpass_wordlist_menu,
"92": download_hashmob_wordlists,
+97
View File
@@ -170,6 +170,103 @@ def quick_crack(ctx: Any) -> None:
)
def loopback_attack(ctx: Any) -> None:
empty_wordlist = os.path.join(ctx.hcatWordlists, "empty.txt")
os.makedirs(ctx.hcatWordlists, exist_ok=True)
if not os.path.exists(empty_wordlist):
with open(empty_wordlist, "w"):
pass
print(f"\nUsing loopback attack with wordlist: {empty_wordlist}")
rule_choice = None
selected_hcatRules = []
rule_files = sorted(
f for f in os.listdir(ctx.rulesDirectory) if f != ".DS_Store"
)
if not rule_files:
download_rules = (
input("\nNo rules found. Download rules from Hashmob now? (Y/n): ")
.strip()
.lower()
)
if download_rules in ("", "y", "yes"):
download_hashmob_rules(print_fn=print)
rule_files = sorted(os.listdir(ctx.rulesDirectory))
if not rule_files:
print("No rules available. Proceeding without rules.")
rule_choice = ["0"]
else:
print("\nWhich rule(s) would you like to run?")
rule_entries = ["0. To run without any rules"]
rule_entries.extend(
[f"{i}. {file}" for i, file in enumerate(rule_files, start=1)]
)
rule_entries.append("98. YOLO...run all of the rules")
rule_entries.append("99. Back to Main Menu")
max_rule_len = max((len(e) for e in rule_entries), default=26)
print_multicolumn_list(
"Available Rules",
rule_entries,
min_col_width=max_rule_len,
max_col_width=max_rule_len,
)
example_line = ""
if len(rule_files) >= 2:
example_line = f"For example 1+1 will run {rule_files[0]} chained twice and 1,2 would run {rule_files[0]} and then {rule_files[1]} sequentially.\n"
elif len(rule_files) == 1:
example_line = f"For example 1+1 will run {rule_files[0]} chained twice.\n"
while rule_choice is None:
raw_choice = input(
"Enter Comma separated list of rules you would like to run. To run rules chained use the + symbol.\n"
f"{example_line}"
"Choose wisely: "
)
if raw_choice.strip() == "99":
return
if raw_choice != "":
rule_choice = raw_choice.split(",")
if "99" in rule_choice:
return
if "98" in rule_choice:
for rule in rule_files:
selected_hcatRules.append(f"-r {ctx.rulesDirectory}/{rule}")
elif "0" in rule_choice:
selected_hcatRules = [""]
else:
for choice in rule_choice:
if "+" in choice:
combined_choice = ""
choices = choice.split("+")
for rule in choices:
try:
combined_choice = f"{combined_choice} -r {ctx.rulesDirectory}/{rule_files[int(rule) - 1]}"
except Exception:
continue
selected_hcatRules.append(combined_choice)
else:
try:
selected_hcatRules.append(
f"-r {ctx.rulesDirectory}/{rule_files[int(choice) - 1]}"
)
except IndexError:
continue
for chain in selected_hcatRules:
ctx.hcatQuickDictionary(
ctx.hcatHashType,
ctx.hcatHashFile,
chain,
empty_wordlist,
loopback=True,
)
def extensive_crack(ctx: Any) -> None:
ctx.hcatBruteForce(ctx.hcatHashType, ctx.hcatHashFile, "1", "7")
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatBruteCount)
+59 -25
View File
@@ -268,6 +268,23 @@ hcatWordlists = config_parser["hcatWordlists"]
hcatOptimizedWordlists = config_parser["hcatOptimizedWordlists"]
hcatRules: list[str] = []
# Optional: override hashcat's default potfile location.
# If unset/empty, we use hashcat's built-in default potfile behavior.
hcatPotfilePath = (config_parser.get("hcatPotfilePath") or "").strip()
if hcatPotfilePath:
hcatPotfilePath = os.path.expanduser(hcatPotfilePath)
if not os.path.isabs(hcatPotfilePath):
hcatPotfilePath = os.path.join(hate_path, hcatPotfilePath)
def _append_potfile_arg(cmd, *, use_potfile_path=True, potfile_path=None):
if not use_potfile_path:
return
pot = potfile_path or hcatPotfilePath
if pot:
cmd.append(f"--potfile-path={pot}")
try:
rulesDirectory = config_parser["rules_directory"]
except KeyError as e:
@@ -834,7 +851,8 @@ def _run_hashcat_show(hash_type, hash_file, output_path):
[
hcatBin,
"--show",
f"--potfile-path={hate_path}/hashcat.pot",
# Use hashcat's built-in potfile unless configured otherwise.
*([f"--potfile-path={hcatPotfilePath}"] if hcatPotfilePath else []),
"-m",
str(hash_type),
hash_file,
@@ -865,7 +883,7 @@ def hcatBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
"?a?a?a?a?a?a?a?a?a?a?a?a?a?a",
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -897,7 +915,7 @@ def hcatDictionary(hcatHashType, hcatHashFile):
cmd.extend(optimized_lists)
cmd.extend(["-r", rule_best66])
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -921,7 +939,7 @@ def hcatDictionary(hcatHashType, hcatHashFile):
rule_d3ad0ne,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -944,7 +962,7 @@ def hcatDictionary(hcatHashType, hcatHashFile):
rule_toxic,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -956,7 +974,15 @@ def hcatDictionary(hcatHashType, hcatHashFile):
# Quick Dictionary Attack (Optional Chained Rules)
def hcatQuickDictionary(hcatHashType, hcatHashFile, hcatChains, wordlists):
def hcatQuickDictionary(
hcatHashType,
hcatHashFile,
hcatChains,
wordlists,
loopback=False,
use_potfile_path=False,
potfile_path=None,
):
global hcatProcess
cmd = [
hcatBin,
@@ -972,10 +998,12 @@ def hcatQuickDictionary(hcatHashType, hcatHashFile, hcatChains, wordlists):
cmd.extend(wordlists)
else:
cmd.append(wordlists)
if loopback:
cmd.append("--loopback")
if hcatChains:
cmd.extend(shlex.split(hcatChains))
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd, use_potfile_path=use_potfile_path, potfile_path=potfile_path)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1040,7 +1068,7 @@ def hcatTopMask(hcatHashType, hcatHashFile, hcatTargetTime):
f"{hcatHashFile}.hcmask",
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1094,7 +1122,7 @@ def hcatFingerprint(hcatHashType, hcatHashFile):
f"{hcatHashFile}.expanded",
f"{hcatHashFile}.expanded",
*shlex.split(hcatTuning),
f"--potfile-path={hate_path}/hashcat.pot",
*([f"--potfile-path={hcatPotfilePath}"] if hcatPotfilePath else []),
]
)
try:
@@ -1125,7 +1153,7 @@ def hcatCombination(hcatHashType, hcatHashFile):
hcatCombinationWordlist[1],
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1182,7 +1210,7 @@ def hcatHybrid(hcatHashType, hcatHashFile, wordlists=None):
*args,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1217,7 +1245,7 @@ def hcatYoloCombination(hcatHashType, hcatHashFile):
right_path,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1266,7 +1294,7 @@ def hcatBandrel(hcatHashType, hcatHashFile):
mask2.strip(),
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1309,7 +1337,7 @@ def hcatBandrel(hcatHashType, hcatHashFile):
mask2.strip(),
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1351,7 +1379,7 @@ def hcatMiddleCombinator(hcatHashType, hcatHashFile):
masks[x],
hcatMiddleBaseList,
hcatMiddleBaseList,
f"--potfile-path={hate_path}/hashcat.pot",
*([f"--potfile-path={hcatPotfilePath}"] if hcatPotfilePath else []),
]
hcatProcess = subprocess.Popen(cmd)
try:
@@ -1395,7 +1423,7 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
hcatThoroughBaseList,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1420,7 +1448,7 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
masks[x],
hcatThoroughBaseList,
hcatThoroughBaseList,
f"--potfile-path={hate_path}/hashcat.pot",
*([f"--potfile-path={hcatPotfilePath}"] if hcatPotfilePath else []),
]
hcatProcess = subprocess.Popen(cmd)
try:
@@ -1450,7 +1478,7 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
hcatThoroughBaseList,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1481,7 +1509,7 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
hcatThoroughBaseList,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
hcatProcess.wait()
except KeyboardInterrupt:
@@ -1506,7 +1534,7 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile):
os.path.join(hate_path, "masks", "pathwell.hcmask"),
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1548,7 +1576,7 @@ def hcatPrince(hcatHashType, hcatHashFile):
prince_rule,
]
hashcat_cmd.extend(shlex.split(hcatTuning))
hashcat_cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(hashcat_cmd)
with open(prince_base, "rb") as base:
prince_proc = subprocess.Popen(prince_cmd, stdin=base, stdout=subprocess.PIPE)
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=prince_proc.stdout)
@@ -1584,7 +1612,7 @@ def hcatGoodMeasure(hcatHashType, hcatHashFile):
hcatGoodMeasureBaseList,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1617,7 +1645,7 @@ def hcatLMtoNT():
"?1?1?1?1?1?1?1",
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1667,7 +1695,7 @@ def hcatLMtoNT():
),
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -1706,7 +1734,7 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords):
rule_path,
]
cmd.extend(shlex.split(hcatTuning))
cmd.append(f"--potfile-path={hate_path}/hashcat.pot")
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
@@ -2412,6 +2440,10 @@ def bandrel_method():
return _attacks.bandrel_method(_attack_ctx())
def loopback_attack():
return _attacks.loopback_attack(_attack_ctx())
# convert hex words for recycling
def convert_hex(working_file):
processed_words = []
@@ -2605,6 +2637,7 @@ def get_main_menu_options():
"11": middle_combinator,
"12": thorough_combinator,
"13": bandrel_method,
"14": loopback_attack,
"90": download_hashmob_rules,
"91": weakpass_wordlist_menu,
"92": download_hashmob_wordlists,
@@ -3138,6 +3171,7 @@ def main():
print("\t(11) Middle Combinator Attack")
print("\t(12) Thorough Combinator Attack")
print("\t(13) Bandrel Methodology")
print("\t(14) Loopback Attack")
print("\n\t(90) Download rules from Hashmob.net")
print("\n\t(91) Download wordlists from Weakpass")
print("\t(92) Download wordlists from Hashmob.net")
+172
View File
@@ -0,0 +1,172 @@
import os
import json
import shutil
import subprocess
import shlex
from pathlib import Path
import pytest
_TEST_HASH = "994a24ad0d9ac6f1fd7d4d75adffeda2"
def _format_hashcat_cmd(cmd: list[str]) -> str:
# Mirror hate_crack's debug printing: safe shell-style quoting.
return " ".join(shlex.quote(part) for part in cmd)
def _get_hcat_tuning_args(repo_root: Path) -> list[str]:
config_path = repo_root / "config.json"
if not config_path.is_file():
return []
try:
config = json.loads(config_path.read_text())
except Exception:
return []
tuning = (config.get("hcatTuning") or "").strip()
if not tuning:
return []
return shlex.split(tuning)
def _hashcat_sessions_writable() -> bool:
"""
Hashcat writes session files under ~/.hashcat/sessions on macOS/homebrew builds.
If that location is not writable (sandbox/MDM), running hashcat will emit stderr.
"""
sessions_dir = Path.home() / ".hashcat" / "sessions"
try:
sessions_dir.mkdir(parents=True, exist_ok=True)
probe = sessions_dir / f"pytest_write_probe_{os.getpid()}"
probe.write_text("probe")
probe.unlink(missing_ok=True)
return True
except Exception:
return False
def _run_hashcat(
cmd: list[str],
cwd: Path,
*,
timeout_s: int = 60,
capsys=None,
show_output: bool = False,
show_cmd: bool = False,
) -> subprocess.CompletedProcess:
"""
Run hashcat and skip (not fail) on common local-environment issues.
This repo's normal test suite is offline/mocked; this test is opt-in and
depends on a local hashcat installation that can write its session files.
"""
if show_cmd and capsys is not None:
with capsys.disabled():
print("\n[DEBUG] hashcat cmd: " + _format_hashcat_cmd(cmd))
try:
result = subprocess.run(
cmd,
cwd=str(cwd),
capture_output=True,
text=True,
timeout=timeout_s,
)
except FileNotFoundError:
pytest.skip("hashcat not available in PATH")
except subprocess.TimeoutExpired:
pytest.fail(f"hashcat timed out after {timeout_s}s: {cmd!r}")
combined = (result.stdout or "") + (result.stderr or "")
if show_output and capsys is not None:
with capsys.disabled():
print("\n[hashcat stdout]\n" + (result.stdout or ""))
print("\n[hashcat stderr]\n" + (result.stderr or ""))
# If hashcat crashed, subprocess uses a negative return code (signal).
if result.returncode < 0:
pytest.fail(
f"hashcat terminated by signal {-result.returncode}. stdout={result.stdout!r} stderr={result.stderr!r}"
)
# Per request: fail on any stderr output (warnings/errors are treated as failure).
if (result.stderr or "").strip():
pytest.fail(
f"hashcat wrote to stderr (treated as failure). cmd={_format_hashcat_cmd(cmd)!r} stderr={result.stderr!r}"
)
assert "Segmentation fault" not in combined
assert "core dumped" not in combined.lower()
return result
def test_toggle_rule_parses_with_and_without_loopback(tmp_path: Path, capsys):
"""
Execute the two hashcat command-lines requested (with an empty wordlist),
primarily to ensure hashcat does not crash while parsing/using the rule file.
"""
if shutil.which("hashcat") is None:
pytest.skip("hashcat not available in PATH")
if not _hashcat_sessions_writable():
pytest.skip("hashcat session directory (~/.hashcat/sessions) is not writable")
show_output = os.environ.get("HATE_CRACK_SHOW_HASHCAT_OUTPUT") == "1"
show_cmd = (
os.environ.get("HATE_CRACK_SHOW_HASHCAT_CMD") == "1"
or os.environ.get("HATE_CRACK_SHOW_HASHCAT_OUTPUT") == "1"
)
repo_root = Path(__file__).resolve().parents[1]
tuning_args = _get_hcat_tuning_args(repo_root)
src_rule = repo_root / "rules" / "toggles-lm-ntlm.rule"
if not src_rule.is_file():
pytest.skip("rules/toggles-lm-ntlm.rule not found")
# Mirror the requested relative `rules/...` path in a temp working dir.
(tmp_path / "rules").mkdir(parents=True, exist_ok=True)
rule_path = tmp_path / "rules" / "toggles-lm-ntlm.rule"
shutil.copy2(src_rule, rule_path)
# Equivalent to: `echo > empty.txt`
(tmp_path / "empty.txt").write_text("")
cmd_with_loopback = [
"hashcat",
*tuning_args,
"-m",
"1000",
"empty.txt",
"--loopback",
"-r",
"rules/toggles-lm-ntlm.rule",
_TEST_HASH,
]
cmd_without_loopback = [
"hashcat",
*tuning_args,
"-m",
"1000",
"empty.txt",
"-r",
"rules/toggles-lm-ntlm.rule",
_TEST_HASH,
]
_run_hashcat(
cmd_with_loopback,
cwd=tmp_path,
capsys=capsys,
show_output=show_output,
show_cmd=show_cmd,
)
_run_hashcat(
cmd_without_loopback,
cwd=tmp_path,
capsys=capsys,
show_output=show_output,
show_cmd=show_cmd,
)
+1
View File
@@ -24,6 +24,7 @@ MENU_OPTION_TEST_CASES = [
("11", CLI_MODULE._attacks, "middle_combinator", "middle"),
("12", CLI_MODULE._attacks, "thorough_combinator", "thorough"),
("13", CLI_MODULE._attacks, "bandrel_method", "bandrel"),
("14", CLI_MODULE._attacks, "loopback_attack", "loopback"),
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),