mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-04-28 12:03:11 -07:00
Merge pull request #96 from trustedsec/feat/random-rules-attack
feat: add random rules attack (#87)
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,3 +15,7 @@ hate_crack/princeprocessor/
|
||||
*.ollama_candidates
|
||||
*.filtered
|
||||
research/
|
||||
--help
|
||||
4_char_all
|
||||
all_hashes.enabled
|
||||
some_histories
|
||||
|
||||
13
README.md
13
README.md
@@ -618,7 +618,9 @@ All tests use mocked API calls, so they can run without connectivity to a Hashvi
|
||||
(16) OMEN Attack
|
||||
(17) Ad-hoc Mask Attack
|
||||
(18) Markov Brute Force Attack
|
||||
(19) Permutation Attack
|
||||
(19) N-gram Attack
|
||||
(20) Permutation Attack
|
||||
(21) Random Rules Attack
|
||||
|
||||
(90) Download rules from Hashmob.net
|
||||
(91) Analyze Hashcat Rules
|
||||
@@ -800,6 +802,14 @@ Generates all character permutations of each word in a targeted wordlist and pip
|
||||
* WARNING: Scales as N! per word - an 8-character word produces 40,320 permutations. Only practical for words up to ~8 characters.
|
||||
* Uses `permute.bin < wordlist | hashcat` pipeline pattern
|
||||
|
||||
#### Random Rules Attack
|
||||
Generates a set of random hashcat mutation rules using `generate-rules.bin`, writes them to a temporary file, then runs hashcat against a chosen wordlist with those rules.
|
||||
|
||||
* Prompts for rule count (default 65536)
|
||||
* Prompts for wordlist path with tab-completion and numbered selection
|
||||
* Temporary rules file is cleaned up after the run regardless of outcome
|
||||
* Useful when known rule sets are exhausted - explores random rule-space for additional cracks
|
||||
|
||||
#### Download Rules from Hashmob.net
|
||||
Downloads the latest rule files from Hashmob.net's rule repository. These rules are curated and optimized for password cracking and can be used with the Quick Crack and Loopback Attack modes.
|
||||
|
||||
@@ -835,6 +845,7 @@ Interactive menu for downloading and managing wordlists from Weakpass.com via Bi
|
||||
### Version History
|
||||
|
||||
Version 2.0+
|
||||
- Added Random Rules Attack (option 20) using `generate-rules.bin` to generate random mutation rules (#87)
|
||||
- Added Ad-hoc Mask Attack (option 17) for user-typed hashcat masks with optional custom character sets
|
||||
- Added Markov Brute Force Attack (option 18) using `hcstat2` statistical tables for password generation
|
||||
- Consolidated Combinator Attacks (formerly options 10/11/12) into interactive submenu under option 6
|
||||
|
||||
Submodule hashcat-utils updated: 64074c5d54...8bbf2baf7b
@@ -90,6 +90,7 @@ def get_main_menu_options():
|
||||
"18": _attacks.markov_brute_force,
|
||||
"19": _attacks.ngram_attack,
|
||||
"20": _attacks.permute_crack,
|
||||
"21": _attacks.generate_rules_crack,
|
||||
"90": download_hashmob_rules,
|
||||
"91": weakpass_wordlist_menu,
|
||||
"92": download_hashmob_wordlists,
|
||||
|
||||
@@ -637,6 +637,84 @@ def markov_brute_force(ctx: Any) -> None:
|
||||
ctx.hcatMarkovBruteForce(ctx.hcatHashType, ctx.hcatHashFile, hcatMinLen, hcatMaxLen)
|
||||
|
||||
|
||||
def generate_rules_crack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("RANDOM RULES ATTACK")
|
||||
print("=" * 60)
|
||||
print("Generates random hashcat mutation rules and applies them to a wordlist.")
|
||||
print("Use when known rulesets are exhausted - a chaos mode for rule-space exploration.")
|
||||
print("=" * 60)
|
||||
|
||||
raw_count = input("\nNumber of random rules to generate (65536): ").strip()
|
||||
try:
|
||||
rule_count = int(raw_count) if raw_count else 65536
|
||||
if rule_count < 1:
|
||||
print("[!] Rule count must be at least 1.")
|
||||
return
|
||||
except ValueError:
|
||||
print("[!] Invalid rule count.")
|
||||
return
|
||||
|
||||
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
|
||||
wordlist_entries = [
|
||||
f"{i}. {file}" for i, file in enumerate(wordlist_files, start=1)
|
||||
]
|
||||
max_entry_len = max((len(e) for e in wordlist_entries), default=24)
|
||||
print_multicolumn_list(
|
||||
"Wordlists",
|
||||
wordlist_entries,
|
||||
min_col_width=max_entry_len,
|
||||
max_col_width=max_entry_len,
|
||||
)
|
||||
|
||||
def path_completer(text, state):
|
||||
base = ctx.hcatWordlists
|
||||
if not text:
|
||||
pattern = os.path.join(base, "*")
|
||||
matches = glob.glob(pattern)
|
||||
else:
|
||||
text = os.path.expanduser(text)
|
||||
if text.startswith(("/", "./", "../", "~")):
|
||||
matches = glob.glob(text + "*")
|
||||
else:
|
||||
pattern = os.path.join(base, text + "*")
|
||||
matches = glob.glob(pattern)
|
||||
matches = [m + "/" if os.path.isdir(m) else m for m in matches]
|
||||
try:
|
||||
return matches[state]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
_configure_readline(path_completer)
|
||||
|
||||
wordlist_choice = None
|
||||
while wordlist_choice is None:
|
||||
try:
|
||||
raw_choice = input(
|
||||
"\nEnter path of wordlist (tab to autocomplete).\n"
|
||||
f"Press Enter for default wordlist directory [{ctx.hcatWordlists}]: "
|
||||
)
|
||||
raw_choice = raw_choice.strip()
|
||||
if raw_choice == "":
|
||||
wordlist_choice = ctx.hcatWordlists
|
||||
elif raw_choice.isdigit() and 1 <= int(raw_choice) <= len(wordlist_files):
|
||||
chosen = os.path.join(
|
||||
ctx.hcatWordlists, wordlist_files[int(raw_choice) - 1]
|
||||
)
|
||||
if os.path.exists(chosen):
|
||||
wordlist_choice = chosen
|
||||
print(wordlist_choice)
|
||||
elif os.path.exists(raw_choice):
|
||||
wordlist_choice = raw_choice
|
||||
else:
|
||||
print("[!] Wordlist not found. Please enter a valid path.")
|
||||
return
|
||||
except ValueError:
|
||||
print("Please enter a valid number.")
|
||||
|
||||
ctx.hcatGenerateRules(ctx.hcatHashType, ctx.hcatHashFile, rule_count, wordlist_choice)
|
||||
|
||||
|
||||
def ngram_attack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("NGRAM ATTACK")
|
||||
|
||||
@@ -687,6 +687,7 @@ hcatNgramXCount = 0
|
||||
hcatHybridCount = 0
|
||||
hcatExtraCount = 0
|
||||
hcatRecycleCount = 0
|
||||
hcatGenerateRulesCount = 0
|
||||
hcatPermuteCount = 0
|
||||
hcatProcess: subprocess.Popen[Any] | None = None
|
||||
debug_mode = False
|
||||
@@ -2734,6 +2735,51 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords):
|
||||
hcatProcess.kill()
|
||||
|
||||
|
||||
def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist):
|
||||
global hcatProcess, hcatGenerateRulesCount
|
||||
generate_rules_path = os.path.join(
|
||||
hate_path, "hashcat-utils", "bin", "generate-rules.bin"
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".rule", prefix="hate_crack_random_", delete=False
|
||||
) as rules_file:
|
||||
rules_path = rules_file.name
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[generate_rules_path, str(rule_count)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
with open(rules_path, "w") as f:
|
||||
f.write(result.stdout)
|
||||
cmd = [
|
||||
hcatBin,
|
||||
"-m",
|
||||
hcatHashType,
|
||||
hcatHashFile,
|
||||
"--session",
|
||||
generate_session_id(),
|
||||
"-o",
|
||||
f"{hcatHashFile}.out",
|
||||
"-r",
|
||||
rules_path,
|
||||
wordlist,
|
||||
]
|
||||
cmd.extend(shlex.split(hcatTuning))
|
||||
_append_potfile_arg(cmd)
|
||||
hcatProcess = subprocess.Popen(cmd)
|
||||
try:
|
||||
hcatProcess.wait()
|
||||
except KeyboardInterrupt:
|
||||
print(f"Killing PID {hcatProcess.pid}...")
|
||||
hcatProcess.kill()
|
||||
finally:
|
||||
if os.path.exists(rules_path):
|
||||
os.unlink(rules_path)
|
||||
hcatGenerateRulesCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
|
||||
|
||||
|
||||
def check_potfile():
|
||||
print("Checking POT file for already cracked hashes...")
|
||||
_run_hashcat_show(hcatHashType, hcatHashFile, f"{hcatHashFile}.out")
|
||||
@@ -3552,6 +3598,10 @@ def omen_attack():
|
||||
return _attacks.omen_attack(_attack_ctx())
|
||||
|
||||
|
||||
def generate_rules_crack():
|
||||
return _attacks.generate_rules_crack(_attack_ctx())
|
||||
|
||||
|
||||
def permute_crack():
|
||||
return _attacks.permute_crack(_attack_ctx())
|
||||
|
||||
@@ -3786,6 +3836,7 @@ def get_main_menu_items():
|
||||
("18", "Markov Brute Force Attack"),
|
||||
("19", "N-gram Attack"),
|
||||
("20", "Permutation Attack"),
|
||||
("21", "Random Rules Attack"),
|
||||
("90", "Download rules from Hashmob.net"),
|
||||
("91", "Analyze Hashcat Rules"),
|
||||
("92", "Download wordlists from Hashmob.net"),
|
||||
@@ -3825,6 +3876,7 @@ def get_main_menu_options():
|
||||
"18": markov_brute_force,
|
||||
"19": ngram_attack,
|
||||
"20": permute_crack,
|
||||
"21": generate_rules_crack,
|
||||
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
|
||||
"91": analyze_rules,
|
||||
"92": download_hashmob_wordlists,
|
||||
|
||||
45
tests/test_random_rules_attack.py
Normal file
45
tests/test_random_rules_attack.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_cli_module():
|
||||
os.environ["HATE_CRACK_SKIP_INIT"] = "1"
|
||||
for key in list(sys.modules.keys()):
|
||||
if "hate_crack" in key:
|
||||
del sys.modules[key]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"hate_crack_cli", PROJECT_ROOT / "hate_crack.py"
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cli():
|
||||
return load_cli_module()
|
||||
|
||||
|
||||
def test_generate_rules_crack_in_main_menu(cli):
|
||||
options = cli.get_main_menu_options()
|
||||
assert "20" in options
|
||||
|
||||
|
||||
def test_generate_rules_crack_handler_calls_main(cli, tmp_path):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = "1000"
|
||||
ctx.hcatHashFile = "/tmp/h.txt"
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
ctx.list_wordlist_files.return_value = []
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("password\n")
|
||||
with patch("builtins.input", side_effect=["100", str(wl)]):
|
||||
cli._attacks.generate_rules_crack(ctx)
|
||||
ctx.hcatGenerateRules.assert_called_once_with("1000", "/tmp/h.txt", 100, str(wl))
|
||||
163
tests/test_random_rules_wrapper.py
Normal file
163
tests/test_random_rules_wrapper.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_module(hc_module):
|
||||
return hc_module._main
|
||||
|
||||
|
||||
def _make_mock_proc(wait_side_effect=None):
|
||||
proc = MagicMock()
|
||||
if wait_side_effect is not None:
|
||||
proc.wait.side_effect = wait_side_effect
|
||||
else:
|
||||
proc.wait.return_value = None
|
||||
proc.pid = 12345
|
||||
return proc
|
||||
|
||||
|
||||
class TestHcatGenerateRules:
|
||||
def test_calls_generate_rules_bin(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("test\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = "l\nu\nc\n"
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0), \
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result) as mock_run, \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
|
||||
main_module.hcatGenerateRules("1000", hash_file, 100, str(wl))
|
||||
|
||||
run_calls = mock_run.call_args_list
|
||||
assert any("generate-rules.bin" in str(c) for c in run_calls)
|
||||
|
||||
def test_calls_hashcat_with_rule_flag(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("test\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = "l\nu\n"
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0), \
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatGenerateRules("1000", hash_file, 100, str(wl))
|
||||
|
||||
popen_calls = mock_popen.call_args_list
|
||||
assert any("-r" in str(c) for c in popen_calls)
|
||||
|
||||
def test_passes_rule_count_to_generate_rules_bin(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("test\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = "l\n"
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0), \
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result) as mock_run, \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
|
||||
main_module.hcatGenerateRules("1000", hash_file, 999, str(wl))
|
||||
|
||||
run_calls = mock_run.call_args_list
|
||||
generate_call = next(
|
||||
(c for c in run_calls if "generate-rules.bin" in str(c)), None
|
||||
)
|
||||
assert generate_call is not None
|
||||
cmd_args = generate_call[0][0]
|
||||
assert "999" in cmd_args
|
||||
|
||||
def test_cleans_up_temp_file(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("test\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = "l\nu\n"
|
||||
captured_paths = []
|
||||
|
||||
import os as _os
|
||||
original_unlink = _os.unlink
|
||||
|
||||
def capturing_unlink(path):
|
||||
captured_paths.append(path)
|
||||
original_unlink(path)
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0), \
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc), \
|
||||
patch("hate_crack.main.os.unlink", side_effect=capturing_unlink):
|
||||
main_module.hcatGenerateRules("1000", hash_file, 50, str(wl))
|
||||
|
||||
assert any("hate_crack_random_" in p for p in captured_paths), \
|
||||
f"Expected temp file cleanup, got: {captured_paths}"
|
||||
|
||||
def test_keyboard_interrupt_kills_process(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("test\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = "l\n"
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0), \
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
|
||||
main_module.hcatGenerateRules("1000", hash_file, 10, str(wl))
|
||||
|
||||
mock_proc.kill.assert_called_once()
|
||||
|
||||
def test_sets_hcatGenerateRulesCount(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("test\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = "l\nu\n"
|
||||
|
||||
# patch.object won't patch reads of module-level globals; set directly
|
||||
original_cracked = main_module.hcatHashCracked
|
||||
main_module.hcatHashCracked = 2
|
||||
try:
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "lineCount", return_value=5), \
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
|
||||
main_module.hcatGenerateRules("1000", hash_file, 10, str(wl))
|
||||
finally:
|
||||
main_module.hcatHashCracked = original_cracked
|
||||
|
||||
assert main_module.hcatGenerateRulesCount == 3 # 5 - 2
|
||||
@@ -28,6 +28,7 @@ MENU_OPTION_TEST_CASES = [
|
||||
("18", CLI_MODULE._attacks, "markov_brute_force", "markov-brute"),
|
||||
("19", CLI_MODULE._attacks, "ngram_attack", "ngram"),
|
||||
("20", CLI_MODULE._attacks, "permute_crack", "permute"),
|
||||
("21", CLI_MODULE._attacks, "generate_rules_crack", "random-rules"),
|
||||
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
|
||||
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
|
||||
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),
|
||||
|
||||
Reference in New Issue
Block a user