mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat: add permutation attack using permute.bin (closes #86)
Adds Permutation Attack (menu option 19) that generates all character permutations of each word in a targeted wordlist and pipes them to hashcat via permute.bin from hashcat-utils. - hcatPermute() in main.py: pipes permute.bin < wordlist | hashcat - permute_crack() in attacks.py: prompts for single wordlist file with factorial-growth warning, tab-autocomplete support - Menu option 19 wired in both main.py and hate_crack.py - hcatPermuteCount tracking alongside other count globals - Tests: test_permute_attack.py (handler behavior) and test_permute_wrapper.py (subprocess wiring) - README: added entry in menu listing and attack descriptions
This commit is contained in:
@@ -618,6 +618,7 @@ 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
|
||||
|
||||
(90) Download rules from Hashmob.net
|
||||
(91) Analyze Hashcat Rules
|
||||
@@ -789,6 +790,14 @@ Generates password candidates using Markov chain statistical models. Similar to
|
||||
* Markov table persists with hash file (filename.out.hcstat2) for fast subsequent runs
|
||||
* Faster than OMEN for general-purpose brute forcing
|
||||
|
||||
#### Permutation Attack
|
||||
Generates all character permutations of each word in a targeted wordlist and pipes them to hashcat via `permute.bin` from hashcat-utils.
|
||||
|
||||
* Prompts for a single wordlist file (not a directory)
|
||||
* Effective against short targeted wordlists where the character set is known but the order is not (company abbreviations, name fragments, known tokens)
|
||||
* 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
|
||||
|
||||
#### 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.
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ def get_main_menu_options():
|
||||
"16": _attacks.omen_attack,
|
||||
"17": _attacks.adhoc_mask_crack,
|
||||
"18": _attacks.markov_brute_force,
|
||||
"19": _attacks.permute_crack,
|
||||
"90": download_hashmob_rules,
|
||||
"91": weakpass_wordlist_menu,
|
||||
"92": download_hashmob_wordlists,
|
||||
|
||||
@@ -620,6 +620,53 @@ def markov_brute_force(ctx: Any) -> None:
|
||||
ctx.hcatMarkovBruteForce(ctx.hcatHashType, ctx.hcatHashFile, hcatMinLen, hcatMaxLen)
|
||||
|
||||
|
||||
def permute_crack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("PERMUTATION ATTACK")
|
||||
print("=" * 60)
|
||||
print("Generates ALL character permutations of each word in a targeted wordlist.")
|
||||
print("WARNING: Scales as N! per word. Only practical for words up to ~8 characters.")
|
||||
print("Best for: short targeted wordlists (names, abbreviations, known fragments).")
|
||||
print("=" * 60)
|
||||
|
||||
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_path = None
|
||||
while wordlist_path is None:
|
||||
raw = input(
|
||||
"\nEnter path to a wordlist FILE (tab to autocomplete): "
|
||||
).strip()
|
||||
if not raw:
|
||||
continue
|
||||
if not os.path.exists(raw):
|
||||
print(f"[!] Path not found: {raw}")
|
||||
continue
|
||||
if os.path.isdir(raw):
|
||||
print("[!] A directory was provided. Please enter a single wordlist file.")
|
||||
continue
|
||||
wordlist_path = raw
|
||||
|
||||
ctx.hcatPermute(ctx.hcatHashType, ctx.hcatHashFile, wordlist_path)
|
||||
|
||||
|
||||
def combinator_submenu(ctx: Any) -> None:
|
||||
from hate_crack.menu import interactive_menu
|
||||
|
||||
|
||||
@@ -677,6 +677,7 @@ hcatCombinationCount = 0
|
||||
hcatHybridCount = 0
|
||||
hcatExtraCount = 0
|
||||
hcatRecycleCount = 0
|
||||
hcatPermuteCount = 0
|
||||
hcatProcess: subprocess.Popen[Any] | None = None
|
||||
debug_mode = False
|
||||
|
||||
@@ -2222,6 +2223,45 @@ def hcatPrince(hcatHashType, hcatHashFile):
|
||||
prince_proc.kill()
|
||||
|
||||
|
||||
def hcatPermute(hcatHashType, hcatHashFile, wordlist):
|
||||
global hcatProcess, hcatPermuteCount
|
||||
permute_path = os.path.join(hate_path, "hashcat-utils", "bin", "permute.bin")
|
||||
if not os.path.isfile(permute_path):
|
||||
print(f"Error: permute.bin not found: {permute_path}")
|
||||
return
|
||||
if not os.path.isfile(wordlist):
|
||||
print(f"Error: wordlist not found: {wordlist}")
|
||||
return
|
||||
hashcat_cmd = [
|
||||
hcatBin,
|
||||
"-m",
|
||||
hcatHashType,
|
||||
hcatHashFile,
|
||||
"--session",
|
||||
generate_session_id(),
|
||||
"-o",
|
||||
f"{hcatHashFile}.out",
|
||||
]
|
||||
hashcat_cmd.extend(shlex.split(hcatTuning))
|
||||
_append_potfile_arg(hashcat_cmd)
|
||||
with open(wordlist, "rb") as wl_file:
|
||||
permute_proc = subprocess.Popen(
|
||||
[permute_path], stdin=wl_file, stdout=subprocess.PIPE
|
||||
)
|
||||
hcatProcess = subprocess.Popen(
|
||||
hashcat_cmd, stdin=permute_proc.stdout
|
||||
)
|
||||
permute_proc.stdout.close()
|
||||
try:
|
||||
hcatProcess.wait()
|
||||
permute_proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
print(f"Killing PID {hcatProcess.pid}...")
|
||||
hcatProcess.kill()
|
||||
permute_proc.kill()
|
||||
hcatPermuteCount = lineCount(f"{hcatHashFile}.out") - hcatHashCracked
|
||||
|
||||
|
||||
# OMEN model directory - writable location for trained model files.
|
||||
# The binaries live in {hate_path}/omen/ (possibly read-only after install),
|
||||
# but model output (createConfig, *.level) goes to ~/.hate_crack/omen/.
|
||||
@@ -3329,6 +3369,10 @@ def omen_attack():
|
||||
return _attacks.omen_attack(_attack_ctx())
|
||||
|
||||
|
||||
def permute_crack():
|
||||
return _attacks.permute_crack(_attack_ctx())
|
||||
|
||||
|
||||
# convert hex words for recycling
|
||||
def convert_hex(working_file):
|
||||
processed_words = []
|
||||
@@ -3557,6 +3601,7 @@ def get_main_menu_items():
|
||||
("16", "OMEN Attack"),
|
||||
("17", "Ad-hoc Mask Attack"),
|
||||
("18", "Markov Brute Force Attack"),
|
||||
("19", "Permutation Attack"),
|
||||
("90", "Download rules from Hashmob.net"),
|
||||
("91", "Analyze Hashcat Rules"),
|
||||
("92", "Download wordlists from Hashmob.net"),
|
||||
@@ -3594,6 +3639,7 @@ def get_main_menu_options():
|
||||
"16": omen_attack,
|
||||
"17": adhoc_mask_crack,
|
||||
"18": markov_brute_force,
|
||||
"19": permute_crack,
|
||||
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
|
||||
"91": analyze_rules,
|
||||
"92": download_hashmob_wordlists,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hate_crack.attacks import permute_crack
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
ctx.hcatWordlists = "/tmp/wordlists"
|
||||
return ctx
|
||||
|
||||
|
||||
class TestPermuteCrack:
|
||||
def test_calls_hcatPermute_with_valid_wordlist(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "target.txt"
|
||||
wl.write_text("abc\ndef\n")
|
||||
with patch("builtins.input", return_value=str(wl)):
|
||||
permute_crack(ctx)
|
||||
ctx.hcatPermute.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(wl)
|
||||
)
|
||||
|
||||
def test_rejects_nonexistent_wordlist_then_accepts_valid(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "real.txt"
|
||||
wl.write_text("test\n")
|
||||
with patch(
|
||||
"builtins.input",
|
||||
side_effect=["/nonexistent/path.txt", str(wl)],
|
||||
):
|
||||
permute_crack(ctx)
|
||||
ctx.hcatPermute.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(wl)
|
||||
)
|
||||
|
||||
def test_rejects_directory_then_accepts_file(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("ab\n")
|
||||
with patch("builtins.input", side_effect=[str(tmp_path), str(wl)]):
|
||||
permute_crack(ctx)
|
||||
ctx.hcatPermute.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(wl)
|
||||
)
|
||||
|
||||
def test_warns_about_factorial_scaling(self, tmp_path, capsys):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
with patch("builtins.input", return_value=str(wl)):
|
||||
permute_crack(ctx)
|
||||
captured = capsys.readouterr()
|
||||
assert "WARNING" in captured.out or "factorial" in captured.out.lower() or "N!" in captured.out
|
||||
|
||||
def test_prints_header(self, tmp_path, capsys):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
with patch("builtins.input", return_value=str(wl)):
|
||||
permute_crack(ctx)
|
||||
captured = capsys.readouterr()
|
||||
assert "PERMUTATION" in captured.out.upper()
|
||||
@@ -0,0 +1,162 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_module(hc_module):
|
||||
"""Return the underlying hate_crack.main module for direct patching."""
|
||||
return hc_module._main
|
||||
|
||||
|
||||
class TestHcatPermute:
|
||||
def test_uses_permute_bin(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
permute_bin = permute_bin_dir / "permute.bin"
|
||||
permute_bin.touch()
|
||||
|
||||
mock_permute_proc = MagicMock()
|
||||
mock_permute_proc.stdout = MagicMock()
|
||||
mock_permute_proc.wait.return_value = None
|
||||
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_hashcat_proc.pid = 99
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
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="sess1"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_permute_proc, mock_hashcat_proc]
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
assert mock_popen.call_count == 2
|
||||
first_call_args = mock_popen.call_args_list[0][0][0]
|
||||
assert "permute.bin" in str(first_call_args)
|
||||
|
||||
def test_pipes_permute_stdout_to_hashcat_stdin(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
(permute_bin_dir / "permute.bin").touch()
|
||||
|
||||
mock_permute_proc = MagicMock()
|
||||
mock_permute_proc.stdout = MagicMock()
|
||||
mock_permute_proc.wait.return_value = None
|
||||
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_hashcat_proc.pid = 99
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
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="sess1"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_permute_proc, mock_hashcat_proc]
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
# Second call (hashcat) should use permute_proc.stdout as stdin
|
||||
second_call_kwargs = mock_popen.call_args_list[1][1]
|
||||
assert second_call_kwargs.get("stdin") == mock_permute_proc.stdout
|
||||
|
||||
def test_hashcat_cmd_includes_hash_type_and_file(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
(permute_bin_dir / "permute.bin").touch()
|
||||
|
||||
mock_permute_proc = MagicMock()
|
||||
mock_permute_proc.stdout = MagicMock()
|
||||
mock_permute_proc.wait.return_value = None
|
||||
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_hashcat_proc.pid = 99
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
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="sess1"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_permute_proc, mock_hashcat_proc]
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
hashcat_cmd = mock_popen.call_args_list[1][0][0]
|
||||
assert "hashcat" in hashcat_cmd
|
||||
assert "-m" in hashcat_cmd
|
||||
assert "1000" in hashcat_cmd
|
||||
assert hash_file in hashcat_cmd
|
||||
|
||||
def test_keyboard_interrupt_kills_both_processes(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
(permute_bin_dir / "permute.bin").touch()
|
||||
|
||||
mock_permute_proc = MagicMock()
|
||||
mock_permute_proc.stdout = MagicMock()
|
||||
mock_permute_proc.wait.return_value = None
|
||||
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.side_effect = KeyboardInterrupt()
|
||||
mock_hashcat_proc.pid = 99
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
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="sess1"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_permute_proc, mock_hashcat_proc]
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
mock_hashcat_proc.kill.assert_called_once()
|
||||
mock_permute_proc.kill.assert_called_once()
|
||||
|
||||
def test_missing_permute_bin_prints_error(self, main_module, tmp_path, capsys):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
# No permute.bin created
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)):
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "permute.bin" in captured.out
|
||||
|
||||
def test_missing_wordlist_prints_error(self, main_module, tmp_path, capsys):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
(permute_bin_dir / "permute.bin").touch()
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)):
|
||||
main_module.hcatPermute("1000", hash_file, "/nonexistent/words.txt")
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "not found" in captured.out.lower() or "error" in captured.out.lower()
|
||||
@@ -26,6 +26,7 @@ MENU_OPTION_TEST_CASES = [
|
||||
("16", CLI_MODULE._attacks, "omen_attack", "omen"),
|
||||
("17", CLI_MODULE._attacks, "adhoc_mask_crack", "adhoc-mask"),
|
||||
("18", CLI_MODULE._attacks, "markov_brute_force", "markov-brute"),
|
||||
("19", CLI_MODULE._attacks, "permute_crack", "permute"),
|
||||
("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