mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-10 06:02:54 -07:00
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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user