mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
Merge pull request #101 from trustedsec/feat/combipow-attack
feat: Passphrase combination attack (issue #88)
This commit is contained in:
@@ -621,6 +621,7 @@ All tests use mocked API calls, so they can run without connectivity to a Hashvi
|
||||
(19) N-gram Attack
|
||||
(20) Permutation Attack
|
||||
(21) Random Rules Attack
|
||||
(22) Combipow Passphrase Attack
|
||||
|
||||
(90) Download rules from Hashmob.net
|
||||
(91) Analyze Hashcat Rules
|
||||
@@ -810,6 +811,15 @@ Generates a set of random hashcat mutation rules using `generate-rules.bin`, wri
|
||||
* 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
|
||||
|
||||
#### Combipow Passphrase Attack
|
||||
Generates all unique non-empty subset combinations from a short wordlist using `combipow.bin` and pipes them into hashcat. Designed for passphrase cracking when you know the pool of words a password was built from.
|
||||
|
||||
* Prompts for a wordlist file (max 63 lines - combipow generates up to 2^n-1 combinations)
|
||||
* Optional space separator (`-s` flag) to insert spaces between words in each combination
|
||||
* Warns if the wordlist exceeds 20 lines (output volume may be large)
|
||||
* Aborts with a clear message if the wordlist exceeds 63 lines (hard limit)
|
||||
* Candidates are piped directly to hashcat stdin
|
||||
|
||||
#### 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.
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ def get_main_menu_options():
|
||||
"19": _attacks.ngram_attack,
|
||||
"20": _attacks.permute_crack,
|
||||
"21": _attacks.generate_rules_crack,
|
||||
"22": _attacks.combipow_crack,
|
||||
"90": download_hashmob_rules,
|
||||
"91": weakpass_wordlist_menu,
|
||||
"92": download_hashmob_wordlists,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import glob
|
||||
import gzip
|
||||
import os
|
||||
import readline
|
||||
from typing import Any
|
||||
@@ -637,6 +638,31 @@ def markov_brute_force(ctx: Any) -> None:
|
||||
ctx.hcatMarkovBruteForce(ctx.hcatHashType, ctx.hcatHashFile, hcatMinLen, hcatMaxLen)
|
||||
|
||||
|
||||
def combipow_crack(ctx: Any) -> None:
|
||||
wordlist = None
|
||||
while wordlist is None:
|
||||
path = input("\n[*] Enter path to wordlist (max 63 lines recommended): ").strip()
|
||||
if not path:
|
||||
continue
|
||||
if not os.path.isfile(path):
|
||||
print(f"[!] File not found: {path}")
|
||||
continue
|
||||
with (gzip.open(path, "rb") if path.endswith(".gz") else open(path, "rb")) as fh:
|
||||
line_count = sum(1 for _ in fh)
|
||||
if line_count > 63:
|
||||
print(
|
||||
f"[!] Wordlist has {line_count} lines (max 63). combipow generates 2^n-1 combinations."
|
||||
)
|
||||
return
|
||||
if line_count > 20:
|
||||
print(
|
||||
f"[*] Warning: {line_count} lines will generate a large number of combinations."
|
||||
)
|
||||
wordlist = path
|
||||
use_space_sep = input("[*] Add spaces between words? (Y/n): ").strip().lower() != "n"
|
||||
ctx.hcatCombipow(ctx.hcatHashType, ctx.hcatHashFile, wordlist, use_space_sep)
|
||||
|
||||
|
||||
def generate_rules_crack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("RANDOM RULES ATTACK")
|
||||
|
||||
@@ -2343,6 +2343,60 @@ def hcatMarkovBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
|
||||
hcatProcess.kill()
|
||||
|
||||
|
||||
# Combipow Passphrase Attack
|
||||
hcatCombipowCount = 0
|
||||
|
||||
|
||||
def hcatCombipow(hcatHashType, hcatHashFile, wordlist, use_space_sep=True):
|
||||
global hcatProcess, hcatCombipowCount
|
||||
hcatCombipowCount += 1
|
||||
combipow_bin = os.path.join(hate_path, "hashcat-utils/bin/combipow.bin")
|
||||
|
||||
tmp_file = None
|
||||
if wordlist.endswith(".gz"):
|
||||
tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt")
|
||||
with gzip.open(wordlist, "rb") as gz_in:
|
||||
tmp_file.write(gz_in.read())
|
||||
tmp_file.close()
|
||||
wordlist_path = tmp_file.name
|
||||
else:
|
||||
wordlist_path = wordlist
|
||||
|
||||
generator_cmd = [combipow_bin]
|
||||
if use_space_sep:
|
||||
generator_cmd.append("-s")
|
||||
generator_cmd.append(wordlist_path)
|
||||
session_name = re.sub(
|
||||
r"[^a-zA-Z0-9_-]", "_", os.path.splitext(os.path.basename(hcatHashFile))[0]
|
||||
)
|
||||
hashcat_cmd = [
|
||||
hcatBin,
|
||||
"--session",
|
||||
session_name,
|
||||
"-m",
|
||||
hcatHashType,
|
||||
hcatHashFile,
|
||||
"-o",
|
||||
f"{hcatHashFile}.out",
|
||||
]
|
||||
hashcat_cmd.extend(shlex.split(hcatTuning))
|
||||
_append_potfile_arg(hashcat_cmd)
|
||||
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
|
||||
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout)
|
||||
generator_proc.stdout.close()
|
||||
try:
|
||||
hcatProcess.wait()
|
||||
generator_proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("Killing PID {0}...".format(str(hcatProcess.pid)))
|
||||
hcatProcess.kill()
|
||||
generator_proc.kill()
|
||||
finally:
|
||||
if tmp_file is not None:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(tmp_file.name)
|
||||
|
||||
|
||||
# PRINCE Attack
|
||||
def hcatPrince(hcatHashType, hcatHashFile):
|
||||
global hcatProcess
|
||||
@@ -3598,6 +3652,10 @@ def omen_attack():
|
||||
return _attacks.omen_attack(_attack_ctx())
|
||||
|
||||
|
||||
def combipow_crack():
|
||||
return _attacks.combipow_crack(_attack_ctx())
|
||||
|
||||
|
||||
def generate_rules_crack():
|
||||
return _attacks.generate_rules_crack(_attack_ctx())
|
||||
|
||||
@@ -3837,6 +3895,7 @@ def get_main_menu_items():
|
||||
("19", "N-gram Attack"),
|
||||
("20", "Permutation Attack"),
|
||||
("21", "Random Rules Attack"),
|
||||
("22", "Combipow Passphrase Attack"),
|
||||
("90", "Download rules from Hashmob.net"),
|
||||
("91", "Analyze Hashcat Rules"),
|
||||
("92", "Download wordlists from Hashmob.net"),
|
||||
@@ -3877,6 +3936,7 @@ def get_main_menu_options():
|
||||
"19": ngram_attack,
|
||||
"20": permute_crack,
|
||||
"21": generate_rules_crack,
|
||||
"22": combipow_crack,
|
||||
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
|
||||
"91": analyze_rules,
|
||||
"92": download_hashmob_wordlists,
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
os.environ["HATE_CRACK_SKIP_INIT"] = "1"
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _load_attacks():
|
||||
"""Import hate_crack.attacks with SKIP_INIT set."""
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import hate_crack.attacks as attacks # noqa: PLC0415
|
||||
|
||||
return attacks
|
||||
|
||||
|
||||
def _load_cli():
|
||||
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()
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
return ctx
|
||||
|
||||
|
||||
# --- Menu presence tests ---
|
||||
|
||||
def test_combipow_crack_in_main_menu(cli):
|
||||
options = cli.get_main_menu_options()
|
||||
assert "22" in options
|
||||
|
||||
|
||||
def test_combipow_crack_menu_item_label():
|
||||
cli = _load_cli()
|
||||
items = cli.get_main_menu_items()
|
||||
keys = [k for k, _ in items]
|
||||
assert "22" in keys
|
||||
labels = {k: label for k, label in items}
|
||||
assert "passphrase" in labels["22"].lower() or "combipow" in labels["22"].lower()
|
||||
|
||||
|
||||
# --- combipow_crack handler tests ---
|
||||
|
||||
class TestCombipowCrack:
|
||||
def test_calls_hcatCombipow_with_space_sep_by_default(self, tmp_path):
|
||||
attacks = _load_attacks()
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("correct\nhorse\nbattery\n")
|
||||
with patch("builtins.input", side_effect=[str(wl), ""]):
|
||||
attacks.combipow_crack(ctx)
|
||||
ctx.hcatCombipow.assert_called_once()
|
||||
call_args = ctx.hcatCombipow.call_args
|
||||
use_space = (
|
||||
call_args[0][3] if len(call_args[0]) > 3 else call_args[1].get("use_space_sep")
|
||||
)
|
||||
assert use_space is True
|
||||
|
||||
def test_calls_hcatCombipow_without_space_sep(self, tmp_path):
|
||||
attacks = _load_attacks()
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("correct\nhorse\nbattery\n")
|
||||
with patch("builtins.input", side_effect=[str(wl), "n"]):
|
||||
attacks.combipow_crack(ctx)
|
||||
ctx.hcatCombipow.assert_called_once()
|
||||
call_args = ctx.hcatCombipow.call_args
|
||||
use_space = (
|
||||
call_args[0][3] if len(call_args[0]) > 3 else call_args[1].get("use_space_sep")
|
||||
)
|
||||
assert use_space is False
|
||||
|
||||
def test_rejects_more_than_63_lines(self, tmp_path):
|
||||
attacks = _load_attacks()
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("\n".join(f"word{i}" for i in range(64)) + "\n")
|
||||
with patch("builtins.input", return_value=str(wl)):
|
||||
attacks.combipow_crack(ctx)
|
||||
ctx.hcatCombipow.assert_not_called()
|
||||
|
||||
def test_accepts_exactly_63_lines(self, tmp_path):
|
||||
attacks = _load_attacks()
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("\n".join(f"word{i}" for i in range(63)) + "\n")
|
||||
with patch("builtins.input", side_effect=[str(wl), ""]):
|
||||
attacks.combipow_crack(ctx)
|
||||
ctx.hcatCombipow.assert_called_once()
|
||||
|
||||
def test_rejects_nonexistent_file(self, tmp_path):
|
||||
attacks = _load_attacks()
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("test\n")
|
||||
with patch("builtins.input", side_effect=["/nonexistent.txt", str(wl), ""]):
|
||||
attacks.combipow_crack(ctx)
|
||||
ctx.hcatCombipow.assert_called_once()
|
||||
|
||||
def test_passes_correct_hash_type_and_file(self, tmp_path):
|
||||
attacks = _load_attacks()
|
||||
ctx = _make_ctx(hash_type="3200", hash_file="/tmp/bcrypt.txt")
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("word1\nword2\n")
|
||||
with patch("builtins.input", side_effect=[str(wl), ""]):
|
||||
attacks.combipow_crack(ctx)
|
||||
ctx.hcatCombipow.assert_called_once()
|
||||
args = ctx.hcatCombipow.call_args[0]
|
||||
assert args[0] == "3200"
|
||||
assert args[1] == "/tmp/bcrypt.txt"
|
||||
assert args[2] == str(wl)
|
||||
|
||||
def test_warns_about_large_word_count(self, tmp_path, capsys):
|
||||
attacks = _load_attacks()
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("\n".join(f"word{i}" for i in range(31)) + "\n")
|
||||
with patch("builtins.input", side_effect=[str(wl), ""]):
|
||||
attacks.combipow_crack(ctx)
|
||||
captured = capsys.readouterr()
|
||||
assert "large" in captured.out.lower() or "warning" in captured.out.lower()
|
||||
|
||||
|
||||
# --- hcatCombipow wrapper tests ---
|
||||
|
||||
def _get_main_module():
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import hate_crack.main as m # noqa: PLC0415
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class TestHcatCombipow:
|
||||
def _run(self, tmp_path, wl, use_space_sep):
|
||||
"""Run hcatCombipow with module globals patched via context managers."""
|
||||
m = _get_main_module()
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
combipow_bin = tmp_path / "hashcat-utils" / "bin" / "combipow.bin"
|
||||
combipow_bin.parent.mkdir(parents=True, exist_ok=True)
|
||||
combipow_bin.touch()
|
||||
|
||||
fake_combipow = MagicMock()
|
||||
fake_combipow.stdout = MagicMock()
|
||||
fake_hashcat = MagicMock()
|
||||
fake_hashcat.pid = 9999
|
||||
|
||||
with (
|
||||
patch.object(m, "hate_path", str(tmp_path)),
|
||||
patch.object(m, "hcatBin", "hashcat"),
|
||||
patch.object(m, "hcatTuning", ""),
|
||||
patch("hate_crack.main.hcatHashFile", hash_file, create=True),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=[fake_combipow, fake_hashcat]) as mock_popen,
|
||||
):
|
||||
m.hcatCombipow("1000", hash_file, str(wl), use_space_sep=use_space_sep)
|
||||
|
||||
return mock_popen
|
||||
|
||||
def test_includes_s_flag_when_use_space_sep_true(self, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("word1\nword2\n")
|
||||
mock_popen = self._run(tmp_path, wl, use_space_sep=True)
|
||||
first_call_args = mock_popen.call_args_list[0][0][0]
|
||||
assert "-s" in first_call_args
|
||||
|
||||
def test_omits_s_flag_when_use_space_sep_false(self, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("word1\nword2\n")
|
||||
mock_popen = self._run(tmp_path, wl, use_space_sep=False)
|
||||
first_call_args = mock_popen.call_args_list[0][0][0]
|
||||
assert "-s" not in first_call_args
|
||||
|
||||
def test_wordlist_passed_as_argument(self, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("word1\nword2\n")
|
||||
mock_popen = self._run(tmp_path, wl, use_space_sep=True)
|
||||
first_call_args = mock_popen.call_args_list[0][0][0]
|
||||
assert str(wl) in first_call_args
|
||||
@@ -29,6 +29,7 @@ MENU_OPTION_TEST_CASES = [
|
||||
("19", CLI_MODULE._attacks, "ngram_attack", "ngram"),
|
||||
("20", CLI_MODULE._attacks, "permute_crack", "permute"),
|
||||
("21", CLI_MODULE._attacks, "generate_rules_crack", "random-rules"),
|
||||
("22", CLI_MODULE._attacks, "combipow_crack", "combipow"),
|
||||
("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