Merge pull request #83 from trustedsec/feat/omen-rules-and-cwd-fix

feat: add rule support to OMEN attack and fix relative path resolution
This commit is contained in:
Justin Bollinger
2026-03-18 18:18:23 -04:00
committed by GitHub
8 changed files with 427 additions and 163 deletions
+2 -2
View File
@@ -99,7 +99,7 @@ install: submodules
@UV_BIN=$$(command -v uv 2>/dev/null || echo "$$HOME/.local/bin/uv"); \
"$$UV_BIN" sync
@mkdir -p "$${XDG_BIN_HOME:-$$HOME/.local/bin}"
@printf '#!/usr/bin/env bash\nset -euo pipefail\nexec uv run --directory %s python -m hate_crack "$$@"\n' "$(CURDIR)" \
@printf '#!/usr/bin/env bash\nset -euo pipefail\nexport HATE_CRACK_ORIG_CWD="$$PWD"\nexec uv run --directory %s python -m hate_crack "$$@"\n' "$(CURDIR)" \
> "$${XDG_BIN_HOME:-$$HOME/.local/bin}/hate_crack"
@chmod +x "$${XDG_BIN_HOME:-$$HOME/.local/bin}/hate_crack"
@echo "Installed hate_crack shim to $${XDG_BIN_HOME:-$$HOME/.local/bin}/hate_crack"
@@ -107,7 +107,7 @@ install: submodules
update: submodules
@uv sync
@mkdir -p "$${XDG_BIN_HOME:-$$HOME/.local/bin}"
@printf '#!/usr/bin/env bash\nset -euo pipefail\nexec uv run --directory %s python -m hate_crack "$$@"\n' "$(CURDIR)" \
@printf '#!/usr/bin/env bash\nset -euo pipefail\nexport HATE_CRACK_ORIG_CWD="$$PWD"\nexec uv run --directory %s python -m hate_crack "$$@"\n' "$(CURDIR)" \
> "$${XDG_BIN_HOME:-$$HOME/.local/bin}/hate_crack"
@chmod +x "$${XDG_BIN_HOME:-$$HOME/.local/bin}/hate_crack"
@echo "Updated hate_crack shim at $${XDG_BIN_HOME:-$$HOME/.local/bin}/hate_crack"
+1 -1
View File
@@ -25,7 +25,7 @@
"ollamaModel": "mistral",
"ollamaNumCtx": 2048,
"omenTrainingList": "rockyou.txt",
"omenMaxCandidates": 1000000,
"omenMaxCandidates": 50000000,
"check_for_updates": true,
"optimizedKernelAttacks": ["hcatDictionary", "hcatQuickDictionary", "hcatBandrel", "hcatGoodMeasure", "hcatRecycle", "hcatBruteForce", "hcatTopMask", "hcatPathwellBruteForce"]
}
+93 -152
View File
@@ -24,10 +24,88 @@ def _configure_readline(completer):
readline.set_completer(completer)
def _select_rules(ctx) -> list[str] | None:
"""Prompt user to select rules. Returns list of rule chain strings, or None if cancelled."""
rule_choice = None
selected_rules = []
rules_dir = ctx.rulesDirectory
rule_files = sorted(f for f in os.listdir(rules_dir) 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, rules_dir=rules_dir)
rule_files = sorted(os.listdir(rules_dir))
if not rule_files:
print("No rules available. Proceeding without rules.")
return [""]
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 None
if raw_choice != "":
rule_choice = raw_choice.split(",")
if "99" in rule_choice:
return None
if "98" in rule_choice:
for rule in rule_files:
selected_rules.append(f"-r {os.path.join(rules_dir, rule)}")
elif "0" in rule_choice:
selected_rules = [""]
else:
for choice in rule_choice:
if "+" in choice:
combined_choice = ""
choices = choice.split("+")
for rule in choices:
try:
rule_path = os.path.join(rules_dir, rule_files[int(rule) - 1])
combined_choice = f"{combined_choice} -r {rule_path}"
except Exception:
continue
selected_rules.append(combined_choice)
else:
try:
rule_path = os.path.join(rules_dir, rule_files[int(choice) - 1])
selected_rules.append(f"-r {rule_path}")
except IndexError:
continue
return selected_rules
def quick_crack(ctx: Any) -> None:
wordlist_choice = None
rule_choice = None
selected_hcatRules = []
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
wordlist_entries = [
@@ -85,81 +163,11 @@ def quick_crack(ctx: Any) -> None:
except ValueError:
print("Please enter a valid number.")
rules_dir = ctx.rulesDirectory
rule_files = sorted(f for f in os.listdir(rules_dir) 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, rules_dir=rules_dir)
rule_files = sorted(os.listdir(rules_dir))
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:
selected_rules = _select_rules(ctx)
if selected_rules is None:
return
if "98" in rule_choice:
for rule in rule_files:
selected_hcatRules.append(f"-r {os.path.join(rules_dir, 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:
rule_path = os.path.join(rules_dir, rule_files[int(rule) - 1])
combined_choice = f"{combined_choice} -r {rule_path}"
except Exception:
continue
selected_hcatRules.append(combined_choice)
else:
try:
rule_path = os.path.join(rules_dir, rule_files[int(choice) - 1])
selected_hcatRules.append(f"-r {rule_path}")
except IndexError:
continue
for chain in selected_hcatRules:
for chain in selected_rules:
ctx.hcatQuickDictionary(
ctx.hcatHashType, ctx.hcatHashFile, chain, wordlist_choice
)
@@ -174,84 +182,11 @@ def loopback_attack(ctx: Any) -> None:
print(f"\nUsing loopback attack with wordlist: {empty_wordlist}")
rule_choice = None
selected_hcatRules = []
rules_dir = ctx.rulesDirectory
rule_files = sorted(f for f in os.listdir(rules_dir) 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, rules_dir=rules_dir)
rule_files = sorted(os.listdir(rules_dir))
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:
selected_rules = _select_rules(ctx)
if selected_rules is None:
return
if "98" in rule_choice:
for rule in rule_files:
selected_hcatRules.append(f"-r {os.path.join(rules_dir, 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:
rule_path = os.path.join(rules_dir, rule_files[int(rule) - 1])
combined_choice = f"{combined_choice} -r {rule_path}"
except Exception:
continue
selected_hcatRules.append(combined_choice)
else:
try:
rule_path = os.path.join(rules_dir, rule_files[int(choice) - 1])
selected_hcatRules.append(f"-r {rule_path}")
except IndexError:
continue
for chain in selected_hcatRules:
for chain in selected_rules:
ctx.hcatQuickDictionary(
ctx.hcatHashType,
ctx.hcatHashFile,
@@ -580,4 +515,10 @@ def omen_attack(ctx: Any) -> None:
).strip()
if not max_candidates:
max_candidates = str(ctx.omenMaxCandidates)
ctx.hcatOmen(ctx.hcatHashType, ctx.hcatHashFile, int(max_candidates))
selected_rules = _select_rules(ctx)
if selected_rules is None:
return
for chain in selected_rules:
ctx.hcatOmen(ctx.hcatHashType, ctx.hcatHashFile, int(max_candidates), chain)
+14 -2
View File
@@ -5,10 +5,22 @@ from typing import Optional
def resolve_path(value: Optional[str]) -> Optional[str]:
"""Expand user and return an absolute path, or None."""
"""Expand user and return an absolute path, or None.
When invoked via the bash shim (``uv run --directory``), the working
directory is changed to the repo root before Python starts.
``HATE_CRACK_ORIG_CWD`` preserves the caller's real working directory
so that relative paths on the command line resolve correctly.
"""
if not value:
return None
return os.path.abspath(os.path.expanduser(value))
value = os.path.expanduser(value)
if os.path.isabs(value):
return value
orig_cwd = os.environ.get("HATE_CRACK_ORIG_CWD")
if orig_cwd:
return os.path.normpath(os.path.join(orig_cwd, value))
return os.path.abspath(value)
def add_common_args(parser) -> None:
+4 -1
View File
@@ -2181,7 +2181,7 @@ def hcatOmenTrain(training_file):
# OMEN Attack - Generate candidates and pipe to hashcat
def hcatOmen(hcatHashType, hcatHashFile, max_candidates):
def hcatOmen(hcatHashType, hcatHashFile, max_candidates, hcatChains=""):
global hcatProcess
omen_dir = _omen_dir
enum_bin = os.path.join(omen_dir, hcatOmenEnumBin)
@@ -2205,8 +2205,11 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates):
"-o",
f"{hcatHashFile}.out",
]
if hcatChains:
hashcat_cmd.extend(shlex.split(hcatChains))
hashcat_cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(hashcat_cmd)
hashcat_cmd = _add_debug_mode_for_rules(hashcat_cmd)
print(f"[*] Running: {_format_cmd(enum_cmd)} | {_format_cmd(hashcat_cmd)}")
_debug_cmd(hashcat_cmd)
enum_proc = subprocess.Popen(
+169 -5
View File
@@ -252,10 +252,11 @@ class TestOmenAttackHandler:
ctx.hcatOmenCreateBin = "createNG"
ctx.hcatOmenEnumBin = "enumNG"
ctx.omenTrainingList = "/default/rockyou.txt"
ctx.omenMaxCandidates = 1000000
ctx.omenMaxCandidates = 50000000
ctx.hcatHashType = "1000"
ctx.hcatHashFile = "/tmp/hashes.txt"
ctx.hcatWordlists = str(tmp_path / "wordlists")
ctx.rulesDirectory = str(tmp_path / "rules")
ctx._omen_model_is_valid.return_value = model_valid
ctx._omen_model_info.return_value = (
{"training_file": "/old/rockyou.txt"} if model_valid else None
@@ -265,10 +266,19 @@ class TestOmenAttackHandler:
ctx.list_wordlist_files.return_value = ["rockyou.txt", "custom.txt"]
return ctx
def _setup_rules_dir(self, tmp_path, rule_names=None):
rules_dir = tmp_path / "rules"
rules_dir.mkdir(exist_ok=True)
if rule_names:
for name in rule_names:
(rules_dir / name).write_text(":")
return rules_dir
def test_use_existing_model(self, tmp_path):
ctx = self._make_ctx(tmp_path, model_valid=True)
self._setup_rules_dir(tmp_path)
with patch("os.path.isfile", return_value=True), patch(
"builtins.input", side_effect=["1", ""]
"builtins.input", side_effect=["1", "", "0"]
):
from hate_crack.attacks import omen_attack
@@ -278,8 +288,9 @@ class TestOmenAttackHandler:
def test_train_new_model_with_wordlist_pick(self, tmp_path):
ctx = self._make_ctx(tmp_path, model_valid=True)
self._setup_rules_dir(tmp_path)
with patch("os.path.isfile", return_value=True), patch(
"builtins.input", side_effect=["2", "1", ""]
"builtins.input", side_effect=["2", "1", "", "0"]
):
from hate_crack.attacks import omen_attack
@@ -302,8 +313,9 @@ class TestOmenAttackHandler:
def test_no_model_goes_straight_to_training(self, tmp_path):
ctx = self._make_ctx(tmp_path, model_valid=False)
self._setup_rules_dir(tmp_path)
with patch("os.path.isfile", return_value=True), patch(
"builtins.input", side_effect=["1", ""]
"builtins.input", side_effect=["1", "", "0"]
):
from hate_crack.attacks import omen_attack
@@ -324,14 +336,166 @@ class TestOmenAttackHandler:
def test_custom_path_for_training(self, tmp_path):
ctx = self._make_ctx(tmp_path, model_valid=False)
self._setup_rules_dir(tmp_path)
with patch("os.path.isfile", return_value=True), patch(
"builtins.input", side_effect=["p", "/custom/wordlist.txt", ""]
"builtins.input", side_effect=["p", "/custom/wordlist.txt", "", "0"]
):
from hate_crack.attacks import omen_attack
omen_attack(ctx)
ctx.hcatOmenTrain.assert_called_once_with("/custom/wordlist.txt")
def test_rules_passed_to_hcatOmen(self, tmp_path):
ctx = self._make_ctx(tmp_path, model_valid=True)
self._setup_rules_dir(tmp_path, ["best64.rule"])
with patch("os.path.isfile", return_value=True), patch(
"builtins.input", side_effect=["1", "", "1"]
):
from hate_crack.attacks import omen_attack
omen_attack(ctx)
call_args = ctx.hcatOmen.call_args
assert "-r" in call_args[0][3]
assert "best64.rule" in call_args[0][3]
def test_multiple_rule_chains_spawn_multiple_calls(self, tmp_path):
ctx = self._make_ctx(tmp_path, model_valid=True)
self._setup_rules_dir(tmp_path, ["best64.rule", "dive.rule"])
with patch("os.path.isfile", return_value=True), patch(
"builtins.input", side_effect=["1", "", "1,2"]
):
from hate_crack.attacks import omen_attack
omen_attack(ctx)
assert ctx.hcatOmen.call_count == 2
def test_cancel_from_rules_aborts(self, tmp_path):
ctx = self._make_ctx(tmp_path, model_valid=True)
self._setup_rules_dir(tmp_path, ["best64.rule"])
with patch("os.path.isfile", return_value=True), patch(
"builtins.input", side_effect=["1", "", "99"]
):
from hate_crack.attacks import omen_attack
omen_attack(ctx)
ctx.hcatOmen.assert_not_called()
def test_no_rules_passes_empty_chain(self, tmp_path):
ctx = self._make_ctx(tmp_path, model_valid=True)
self._setup_rules_dir(tmp_path, ["best64.rule"])
with patch("os.path.isfile", return_value=True), patch(
"builtins.input", side_effect=["1", "", "0"]
):
from hate_crack.attacks import omen_attack
omen_attack(ctx)
ctx.hcatOmen.assert_called_once()
assert ctx.hcatOmen.call_args[0][3] == ""
class TestHcatOmenWithRules:
def test_rule_flags_appear_in_hashcat_command(self, main_module, tmp_path):
omen_dir = tmp_path / "omen"
omen_dir.mkdir()
enum_bin = omen_dir / "enumNG"
enum_bin.touch()
enum_bin.chmod(0o755)
model_dir = tmp_path / "model"
model_dir.mkdir()
(model_dir / "createConfig").write_text("# test config\n")
with patch.object(main_module, "_omen_dir", str(omen_dir)), \
patch.object(main_module, "hcatOmenEnumBin", "enumNG"), \
patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatDebugLogPath", str(tmp_path / "debug")), \
patch.object(main_module, "hcatHashFile", "/tmp/hashes.txt", create=True), \
patch("hate_crack.main._omen_model_dir", return_value=str(model_dir)), \
patch("hate_crack.main.subprocess.Popen") as mock_popen:
mock_enum_proc = MagicMock()
mock_enum_proc.stdout = MagicMock()
mock_enum_proc.stderr = MagicMock()
mock_enum_proc.stderr.read.return_value = b""
mock_enum_proc.returncode = 0
mock_enum_proc.wait.return_value = None
mock_hashcat_proc = MagicMock()
mock_hashcat_proc.wait.return_value = None
mock_popen.side_effect = [mock_enum_proc, mock_hashcat_proc]
main_module.hcatOmen("1000", "/tmp/hashes.txt", 500000, "-r /tmp/best64.rule")
hashcat_cmd = mock_popen.call_args_list[1][0][0]
assert "-r" in hashcat_cmd
assert "/tmp/best64.rule" in hashcat_cmd
def test_debug_mode_added_when_rules_present(self, main_module, tmp_path):
omen_dir = tmp_path / "omen"
omen_dir.mkdir()
enum_bin = omen_dir / "enumNG"
enum_bin.touch()
enum_bin.chmod(0o755)
model_dir = tmp_path / "model"
model_dir.mkdir()
(model_dir / "createConfig").write_text("# test config\n")
with patch.object(main_module, "_omen_dir", str(omen_dir)), \
patch.object(main_module, "hcatOmenEnumBin", "enumNG"), \
patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatDebugLogPath", str(tmp_path / "debug")), \
patch.object(main_module, "hcatHashFile", "/tmp/hashes.txt", create=True), \
patch("hate_crack.main._omen_model_dir", return_value=str(model_dir)), \
patch("hate_crack.main.subprocess.Popen") as mock_popen:
mock_enum_proc = MagicMock()
mock_enum_proc.stdout = MagicMock()
mock_enum_proc.stderr = MagicMock()
mock_enum_proc.stderr.read.return_value = b""
mock_enum_proc.returncode = 0
mock_enum_proc.wait.return_value = None
mock_hashcat_proc = MagicMock()
mock_hashcat_proc.wait.return_value = None
mock_popen.side_effect = [mock_enum_proc, mock_hashcat_proc]
main_module.hcatOmen("1000", "/tmp/hashes.txt", 500000, "-r /tmp/best64.rule")
hashcat_cmd = mock_popen.call_args_list[1][0][0]
assert "--debug-mode" in hashcat_cmd
def test_no_rules_no_debug_mode(self, main_module, tmp_path):
omen_dir = tmp_path / "omen"
omen_dir.mkdir()
enum_bin = omen_dir / "enumNG"
enum_bin.touch()
enum_bin.chmod(0o755)
model_dir = tmp_path / "model"
model_dir.mkdir()
(model_dir / "createConfig").write_text("# test config\n")
with patch.object(main_module, "_omen_dir", str(omen_dir)), \
patch.object(main_module, "hcatOmenEnumBin", "enumNG"), \
patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatHashFile", "/tmp/hashes.txt", create=True), \
patch("hate_crack.main._omen_model_dir", return_value=str(model_dir)), \
patch("hate_crack.main.subprocess.Popen") as mock_popen:
mock_enum_proc = MagicMock()
mock_enum_proc.stdout = MagicMock()
mock_enum_proc.stderr = MagicMock()
mock_enum_proc.stderr.read.return_value = b""
mock_enum_proc.returncode = 0
mock_enum_proc.wait.return_value = None
mock_hashcat_proc = MagicMock()
mock_hashcat_proc.wait.return_value = None
mock_popen.side_effect = [mock_enum_proc, mock_hashcat_proc]
main_module.hcatOmen("1000", "/tmp/hashes.txt", 500000)
hashcat_cmd = mock_popen.call_args_list[1][0][0]
assert "--debug-mode" not in hashcat_cmd
class TestOmenModelValidation:
@pytest.fixture
+21
View File
@@ -15,6 +15,27 @@ def test_resolve_path_none_and_expand():
assert os.path.isabs(resolved)
def test_resolve_path_uses_orig_cwd_for_relative_paths(monkeypatch, tmp_path):
"""When HATE_CRACK_ORIG_CWD is set, relative paths resolve against it."""
monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(tmp_path))
result = cli.resolve_path("hashes.txt")
assert result == os.path.join(str(tmp_path), "hashes.txt")
def test_resolve_path_ignores_orig_cwd_for_absolute_paths(monkeypatch, tmp_path):
"""Absolute paths are returned as-is regardless of HATE_CRACK_ORIG_CWD."""
monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(tmp_path))
result = cli.resolve_path("/absolute/path/hashes.txt")
assert result == "/absolute/path/hashes.txt"
def test_resolve_path_without_orig_cwd_uses_abspath(monkeypatch):
"""Without HATE_CRACK_ORIG_CWD, falls back to os.path.abspath."""
monkeypatch.delenv("HATE_CRACK_ORIG_CWD", raising=False)
result = cli.resolve_path("hashes.txt")
assert result == os.path.abspath("hashes.txt")
def test_setup_logging_adds_single_streamhandler(tmp_path):
logger = logging.getLogger("hate_crack_test")
logger.handlers.clear()
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
import sys
import os
import subprocess
import shutil
import pathlib
def usage():
print(f"usage: python {sys.argv[0]} <input file list> <output directory>")
def main():
try:
if not os.path.isfile(sys.argv[1]):
print(f"{sys.argv[1]} is not a valid file.")
sys.exit(1)
if not os.path.isdir(sys.argv[2]):
create_directory = input(
f"{sys.argv[2]} is not a directory. Do you want to create it? (Y or N) "
)
if create_directory.upper() == "Y":
try:
pathlib.Path(sys.argv[2]).mkdir(parents=True, exist_ok=True)
except PermissionError:
print(
"You do not have the correct permissions to create the directory. "
"Please try a different path or create manually."
)
sys.exit(1)
else:
print("Please specify a valid directory and try again.")
sys.exit(1)
input_list = open(sys.argv[1], "r")
destination = sys.argv[2]
except IndexError:
usage()
sys.exit(1)
# Resolve binary paths relative to script location
script_dir = os.path.dirname(os.path.realpath(__file__))
ext = ".app" if sys.platform == "darwin" else ".bin"
splitlen_bin = os.path.join(script_dir, "hashcat-utils", "bin", f"splitlen{ext}")
rli_bin = os.path.join(script_dir, "hashcat-utils", "bin", f"rli{ext}")
# Verify binaries exist
for binary_path, binary_name in [
(splitlen_bin, "splitlen"),
(rli_bin, "rli"),
]:
if not os.path.isfile(binary_path):
print(
f"Error: {binary_name} binary not found at {binary_path}. "
"Ensure hashcat-utils is built."
)
sys.exit(1)
# Get list of wordlists from <input file list> argument
for wordlist in input_list:
wordlist = wordlist.strip()
if not wordlist:
continue
print(wordlist)
# Parse wordlists by password length into "optimized" <output directory>
if len(os.listdir(destination)) == 0:
with open(wordlist, "r") as wl:
try:
subprocess.run(
[splitlen_bin, destination],
stdin=wl,
check=True,
capture_output=True,
)
except subprocess.CalledProcessError as e:
print(f"Error running splitlen on {wordlist}: {e.stderr.decode()}")
sys.exit(1)
else:
if not os.path.isdir("/tmp/splitlen"):
os.mkdir("/tmp/splitlen")
with open(wordlist, "r") as wl:
try:
subprocess.run(
[splitlen_bin, "/tmp/splitlen"],
stdin=wl,
check=True,
capture_output=True,
)
except subprocess.CalledProcessError as e:
print(f"Error running splitlen on {wordlist}: {e.stderr.decode()}")
sys.exit(1)
# Copy unique passwords into "optimized" <output directory>
for file in os.listdir("/tmp/splitlen"):
src_file = os.path.join("/tmp/splitlen", file)
dst_file = os.path.join(destination, file)
if not os.path.isfile(dst_file):
shutil.copyfile(src_file, dst_file)
else:
try:
subprocess.run(
[rli_bin, src_file, "/tmp/splitlen.out", dst_file],
check=True,
capture_output=True,
)
except subprocess.CalledProcessError as e:
print(f"Error running rli on {file}: {e.stderr.decode()}")
sys.exit(1)
if os.path.getsize("/tmp/splitlen.out") > 0:
with open(dst_file, "a") as dst:
with open("/tmp/splitlen.out", "r") as src:
dst.write(src.read())
# Clean Up
if os.path.isdir("/tmp/splitlen"):
shutil.rmtree("/tmp/splitlen")
if os.path.isfile("/tmp/splitlen.out"):
os.remove("/tmp/splitlen.out")
if __name__ == "__main__":
main()