mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-04-28 12:03:11 -07:00
feat: add combipow passphrase attack at menu key 22
Resolves merge conflict with origin/main (keys 19-21 used by ngram, permute, random-rules). Combipow takes key 22. - gzip support: decompress .gz wordlists to a temp file before passing path to combipow.bin (which requires a filename argument) - UI line-counting uses gzip.open for .gz files - Update tests to reference key 22 instead of 21
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
|
||||
|
||||
26
README.md
26
README.md
@@ -618,7 +618,10 @@ 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
|
||||
(21) Combipow Passphrase Attack
|
||||
(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
|
||||
@@ -764,11 +767,13 @@ Uses the Ordered Markov ENumerator (OMEN) to train a statistical password model
|
||||
* Model files and metadata are stored in `~/.hate_crack/omen/` for persistence across sessions
|
||||
|
||||
#### Combinator Attacks Submenu
|
||||
Opens an interactive submenu with four combinator attack variants (formerly at menu keys 10-12). Consolidates related attacks for cleaner menu organization:
|
||||
Opens an interactive submenu with six combinator attack variants (formerly at menu keys 10-12). Consolidates related attacks for cleaner menu organization:
|
||||
- Combinator Attack - combines two wordlists
|
||||
- YOLO Combinator Attack - combines all permutations of multiple wordlists
|
||||
- Middle Combinator Attack - combines wordlists with an extra word in the middle
|
||||
- Thorough Combinator Attack - comprehensive combination of wordlists with rules
|
||||
- Combinator3 Attack - combines exactly 3 wordlists using `combinator3.bin`, generating all `word1+word2+word3` combinations piped to hashcat
|
||||
- CombinatorX Attack - combines 2-8 wordlists using `combinatorX.bin` with optional `--sepFill` separator character between word segments
|
||||
|
||||
#### Ad-hoc Mask Attack
|
||||
Runs hashcat mask attack (mode 3) with a user-specified custom mask string. Allows fine-grained control over character-set brute forcing.
|
||||
@@ -790,6 +795,22 @@ 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
|
||||
|
||||
#### 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
|
||||
|
||||
#### 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.
|
||||
|
||||
@@ -834,6 +855,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
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"rules_directory": "./hashcat/rules",
|
||||
"hcatDictionaryWordlist": ["rockyou.txt"],
|
||||
"hcatCombinationWordlist": ["rockyou.txt","rockyou.txt"],
|
||||
"hcatCombinator3Wordlist": ["rockyou.txt","rockyou.txt","rockyou.txt"],
|
||||
"hcatCombinatorXWordlist": ["rockyou.txt","rockyou.txt"],
|
||||
"hcatHybridlist": ["rockyou.txt"],
|
||||
"hcatMiddleCombinatorMasks": ["2","4"," ","-","_","+",",",".","&"],
|
||||
"hcatMiddleBaseList": "rockyou.txt",
|
||||
|
||||
@@ -88,7 +88,10 @@ def get_main_menu_options():
|
||||
"16": _attacks.omen_attack,
|
||||
"17": _attacks.adhoc_mask_crack,
|
||||
"18": _attacks.markov_brute_force,
|
||||
"21": _attacks.combipow_crack,
|
||||
"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,10 +1,12 @@
|
||||
import glob
|
||||
import gzip
|
||||
import os
|
||||
import readline
|
||||
from typing import Any
|
||||
|
||||
from hate_crack.api import download_hashmob_rules
|
||||
from hate_crack.formatting import print_multicolumn_list
|
||||
from hate_crack.menu import interactive_menu
|
||||
|
||||
|
||||
def _configure_readline(completer):
|
||||
@@ -261,8 +263,7 @@ def combinator_crack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("COMBINATOR ATTACK")
|
||||
print("=" * 60)
|
||||
print("This attack combines two wordlists to generate candidates.")
|
||||
print("Example: wordlist1='password' + wordlist2='123' = 'password123'")
|
||||
print("Combines 2-8 wordlists. 2 uses hashcat native mode; 3+ use external binaries.")
|
||||
print("=" * 60)
|
||||
|
||||
use_default = (
|
||||
@@ -270,73 +271,30 @@ def combinator_crack(ctx: Any) -> None:
|
||||
)
|
||||
|
||||
if use_default != "n":
|
||||
print("\nUsing default wordlist(s) from config:")
|
||||
if isinstance(ctx.hcatCombinationWordlist, list):
|
||||
for wl in ctx.hcatCombinationWordlist:
|
||||
print(f" - {wl}")
|
||||
wordlists = ctx.hcatCombinationWordlist
|
||||
else:
|
||||
print(f" - {ctx.hcatCombinationWordlist}")
|
||||
wordlists = [ctx.hcatCombinationWordlist]
|
||||
else:
|
||||
print("\nSelect wordlists for combinator attack.")
|
||||
print("You need to provide exactly 2 wordlists.")
|
||||
print("You can enter:")
|
||||
print(" - Two file paths separated by commas")
|
||||
print(" - Press TAB to autocomplete file paths")
|
||||
|
||||
selection = ctx.select_file_with_autocomplete(
|
||||
"Enter 2 wordlist files (comma-separated)",
|
||||
allow_multiple=True,
|
||||
base_dir=ctx.hcatWordlists,
|
||||
)
|
||||
|
||||
if not selection:
|
||||
print("No wordlists selected. Aborting combinator attack.")
|
||||
base = ctx.hcatCombinationWordlist
|
||||
wordlists = base if isinstance(base, list) else [base]
|
||||
wordlists = [ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists]
|
||||
if len(wordlists) < 2:
|
||||
print("\n[!] Config does not have at least 2 wordlists.")
|
||||
print("Set hcatCombinationWordlist to a list of 2+ paths in config.json.")
|
||||
print("Aborting combinator attack.")
|
||||
return
|
||||
|
||||
if isinstance(selection, str):
|
||||
wordlists = [selection]
|
||||
else:
|
||||
wordlists = selection
|
||||
|
||||
separator = ""
|
||||
else:
|
||||
print("\nEnter 2-8 wordlists. Enter a blank line when done.")
|
||||
wordlists = _prompt_wordlist_paths(ctx, max_count=8)
|
||||
if len(wordlists) < 2:
|
||||
print("\n[!] Combinator attack requires at least 2 wordlists.")
|
||||
print("Aborting combinator attack.")
|
||||
return
|
||||
separator = input("\nEnter separator between words (leave blank for none): ").strip()
|
||||
|
||||
valid_wordlists = []
|
||||
for wl in wordlists[:2]: # Only use first 2
|
||||
resolved = ctx._resolve_wordlist_path(wl, ctx.hcatWordlists)
|
||||
if os.path.isfile(resolved):
|
||||
valid_wordlists.append(resolved)
|
||||
print(f"✓ Found: {resolved}")
|
||||
else:
|
||||
print(f"✗ Not found: {resolved}")
|
||||
|
||||
if len(valid_wordlists) < 2:
|
||||
print("\nCould not find 2 valid wordlists. Aborting combinator attack.")
|
||||
return
|
||||
|
||||
wordlists = valid_wordlists
|
||||
|
||||
wordlists = [
|
||||
ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists[:2]
|
||||
]
|
||||
|
||||
if len(wordlists) < 2:
|
||||
print("\n[!] Combinator attack requires 2 wordlists but only 1 is configured.")
|
||||
print("Set hcatCombinationWordlist to a list of 2 paths in config.json.")
|
||||
print("Aborting combinator attack.")
|
||||
return
|
||||
|
||||
print("\nStarting combinator attack with 2 wordlists:")
|
||||
print(f" Wordlist 1: {wordlists[0]}")
|
||||
print(f" Wordlist 2: {wordlists[1]}")
|
||||
print(f"Hash type: {ctx.hcatHashType}")
|
||||
print(f"Hash file: {ctx.hcatHashFile}")
|
||||
|
||||
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
if len(wordlists) == 2 and not separator:
|
||||
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
elif len(wordlists) == 3 and not separator:
|
||||
ctx.hcatCombinator3(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
else:
|
||||
ctx.hcatCombinatorX(ctx.hcatHashType, ctx.hcatHashFile, wordlists, separator or None)
|
||||
|
||||
|
||||
def hybrid_crack(ctx: Any) -> None:
|
||||
@@ -427,6 +385,66 @@ def middle_combinator(ctx: Any) -> None:
|
||||
ctx.hcatMiddleCombinator(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
|
||||
def _prompt_wordlist_paths(ctx, max_count: int) -> list[str]:
|
||||
"""Prompt for wordlist paths one at a time with tab-autocomplete.
|
||||
|
||||
Stops when a blank line is entered or max_count paths have been collected.
|
||||
Returns a list of resolved, valid file paths.
|
||||
"""
|
||||
|
||||
def path_completer(text, state):
|
||||
base = ctx.hcatWordlists
|
||||
if not text:
|
||||
pattern = os.path.join(base, "*")
|
||||
matches = glob.glob(pattern)
|
||||
else:
|
||||
expanded = os.path.expanduser(text)
|
||||
if expanded.startswith(("/", "./", "../", "~")):
|
||||
matches = glob.glob(expanded + "*")
|
||||
else:
|
||||
pattern = os.path.join(base, expanded + "*")
|
||||
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)
|
||||
|
||||
collected: list[str] = []
|
||||
count = 1
|
||||
while len(collected) < max_count:
|
||||
raw = input(
|
||||
f"\nWordlist #{count} (tab to autocomplete, blank to finish): "
|
||||
).strip()
|
||||
if not raw:
|
||||
break
|
||||
resolved = ctx._resolve_wordlist_path(raw, ctx.hcatWordlists)
|
||||
if os.path.isfile(resolved):
|
||||
collected.append(resolved)
|
||||
print(f"Added: {resolved}")
|
||||
count += 1
|
||||
else:
|
||||
print(f"Not found: {resolved}")
|
||||
return collected
|
||||
|
||||
|
||||
def combinator3_crack(ctx: Any) -> None:
|
||||
"""3-way combinator attack (delegates to unified combinator_crack)."""
|
||||
combinator_crack(ctx)
|
||||
|
||||
|
||||
def combinatorX_crack(ctx: Any) -> None:
|
||||
"""N-way combinator attack (delegates to unified combinator_crack)."""
|
||||
combinator_crack(ctx)
|
||||
|
||||
|
||||
def combinator_3plus_crack(ctx: Any) -> None:
|
||||
"""3+ wordlist combinator (delegates to unified combinator_crack)."""
|
||||
combinator_crack(ctx)
|
||||
|
||||
|
||||
def bandrel_method(ctx: Any) -> None:
|
||||
ctx.hcatBandrel(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
@@ -629,7 +647,7 @@ def combipow_crack(ctx: Any) -> None:
|
||||
if not os.path.isfile(path):
|
||||
print(f"[!] File not found: {path}")
|
||||
continue
|
||||
with open(path) as fh:
|
||||
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(
|
||||
@@ -645,11 +663,160 @@ def combipow_crack(ctx: Any) -> None:
|
||||
ctx.hcatCombipow(ctx.hcatHashType, ctx.hcatHashFile, wordlist, use_space_sep)
|
||||
|
||||
|
||||
def combinator_submenu(ctx: Any) -> None:
|
||||
from hate_crack.menu import interactive_menu
|
||||
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")
|
||||
print("=" * 60)
|
||||
print("Generates n-gram candidates from a corpus file via ngramX.bin.")
|
||||
print("Gzip-compressed corpus files are auto-detected and decompressed.")
|
||||
print("=" * 60)
|
||||
|
||||
corpus = ctx.select_file_with_autocomplete(
|
||||
"Select corpus file (tab to autocomplete)",
|
||||
base_dir=ctx.hcatWordlists,
|
||||
)
|
||||
if not corpus:
|
||||
print("No corpus selected. Aborting ngram attack.")
|
||||
return
|
||||
|
||||
group_size_raw = input("\nEnter n-gram group size (default 3): ").strip()
|
||||
try:
|
||||
group_size = int(group_size_raw) if group_size_raw else 3
|
||||
except ValueError:
|
||||
print("[!] Invalid group size. Using default of 3.")
|
||||
group_size = 3
|
||||
|
||||
ctx.hcatNgramX(ctx.hcatHashType, ctx.hcatHashFile, corpus, group_size)
|
||||
|
||||
|
||||
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:
|
||||
items = [
|
||||
("1", "Combinator Attack"),
|
||||
("1", "Combinator Attack (2-8 wordlists)"),
|
||||
("2", "YOLO Combinator Attack"),
|
||||
("3", "Middle Combinator Attack"),
|
||||
("4", "Thorough Combinator Attack"),
|
||||
|
||||
@@ -23,7 +23,10 @@ import time
|
||||
import argparse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import contextlib
|
||||
import gzip
|
||||
import lzma
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
|
||||
#!/usr/bin/env python3
|
||||
@@ -356,19 +359,19 @@ else:
|
||||
|
||||
|
||||
def _append_potfile_arg(cmd, *, use_potfile_path=True, potfile_path=None):
|
||||
if not use_potfile_path:
|
||||
return
|
||||
pot = potfile_path or hcatPotfilePath
|
||||
if pot:
|
||||
try:
|
||||
pot_dir = os.path.dirname(pot)
|
||||
if pot_dir:
|
||||
os.makedirs(pot_dir, exist_ok=True)
|
||||
if not os.path.exists(pot):
|
||||
open(pot, "a").close()
|
||||
except OSError:
|
||||
pass
|
||||
cmd.append(f"--potfile-path={pot}")
|
||||
if use_potfile_path:
|
||||
pot = potfile_path or hcatPotfilePath
|
||||
if pot:
|
||||
try:
|
||||
pot_dir = os.path.dirname(pot)
|
||||
if pot_dir:
|
||||
os.makedirs(pot_dir, exist_ok=True)
|
||||
if not os.path.exists(pot):
|
||||
open(pot, "a").close()
|
||||
except OSError:
|
||||
pass
|
||||
cmd.append(f"--potfile-path={pot}")
|
||||
_debug_cmd(cmd)
|
||||
|
||||
|
||||
rulesDirectory = config_parser["rules_directory"]
|
||||
@@ -401,6 +404,8 @@ pipalPath = config_parser["pipalPath"]
|
||||
hcatDictionaryWordlist = config_parser["hcatDictionaryWordlist"]
|
||||
hcatHybridlist = config_parser["hcatHybridlist"]
|
||||
hcatCombinationWordlist = config_parser["hcatCombinationWordlist"]
|
||||
hcatCombinator3Wordlist = config_parser.get("hcatCombinator3Wordlist", ["rockyou.txt", "rockyou.txt", "rockyou.txt"])
|
||||
hcatCombinatorXWordlist = config_parser.get("hcatCombinatorXWordlist", ["rockyou.txt", "rockyou.txt"])
|
||||
hcatMiddleCombinatorMasks = config_parser["hcatMiddleCombinatorMasks"]
|
||||
hcatMiddleBaseList = config_parser["hcatMiddleBaseList"]
|
||||
hcatThoroughCombinatorMasks = config_parser["hcatThoroughCombinatorMasks"]
|
||||
@@ -558,6 +563,8 @@ hcatDictionaryWordlist = _normalize_wordlist_setting(
|
||||
hcatCombinationWordlist = _normalize_wordlist_setting(
|
||||
hcatCombinationWordlist, wordlists_dir
|
||||
)
|
||||
hcatCombinator3Wordlist = _normalize_wordlist_setting(hcatCombinator3Wordlist, wordlists_dir)
|
||||
hcatCombinatorXWordlist = _normalize_wordlist_setting(hcatCombinatorXWordlist, wordlists_dir)
|
||||
hcatHybridlist = _normalize_wordlist_setting(hcatHybridlist, wordlists_dir)
|
||||
hcatMiddleBaseList = _normalize_wordlist_setting(hcatMiddleBaseList, wordlists_dir)
|
||||
hcatThoroughBaseList = _normalize_wordlist_setting(hcatThoroughBaseList, wordlists_dir)
|
||||
@@ -674,13 +681,25 @@ hcatDictionaryCount = 0
|
||||
hcatMaskCount = 0
|
||||
hcatFingerprintCount = 0
|
||||
hcatCombinationCount = 0
|
||||
hcatCombinator3Count = 0
|
||||
hcatCombinatorXCount = 0
|
||||
hcatNgramXCount = 0
|
||||
hcatHybridCount = 0
|
||||
hcatExtraCount = 0
|
||||
hcatRecycleCount = 0
|
||||
hcatGenerateRulesCount = 0
|
||||
hcatPermuteCount = 0
|
||||
hcatProcess: subprocess.Popen[Any] | None = None
|
||||
debug_mode = False
|
||||
|
||||
|
||||
def _open_wordlist(path):
|
||||
"""Open a wordlist file, transparently decompressing gzip if the path ends with .gz."""
|
||||
if path.endswith(".gz"):
|
||||
return gzip.open(path, "rb")
|
||||
return open(path, "rb")
|
||||
|
||||
|
||||
def _format_cmd(cmd):
|
||||
# Shell-style quoting to mirror what a user could run in a terminal.
|
||||
return " ".join(shlex.quote(str(part)) for part in cmd)
|
||||
@@ -691,6 +710,37 @@ def _debug_cmd(cmd):
|
||||
print(f"[DEBUG] hashcat cmd: {_format_cmd(cmd)}")
|
||||
|
||||
|
||||
def _is_gzipped(path: str) -> bool:
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
return f.read(2) == b"\x1f\x8b"
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _wordlist_path(path: str):
|
||||
"""Yield an uncompressed path for path.
|
||||
|
||||
If the file is gzip-compressed, decompress to a temp file and clean up on
|
||||
exit. Otherwise yield the original path unchanged.
|
||||
"""
|
||||
if _is_gzipped(path):
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as tmp:
|
||||
tmp_name = tmp.name
|
||||
with gzip.open(path, "rb") as gz_in:
|
||||
shutil.copyfileobj(gz_in, tmp)
|
||||
try:
|
||||
yield tmp_name
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp_name)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
yield path
|
||||
|
||||
|
||||
def _add_debug_mode_for_rules(cmd):
|
||||
"""Add debug mode arguments to hashcat command if rules are being used.
|
||||
|
||||
@@ -1415,6 +1465,125 @@ def hcatCombination(hcatHashType, hcatHashFile, wordlists=None):
|
||||
hcatCombinationCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
|
||||
|
||||
|
||||
# Combinator3 Attack - 3-way combination via combinator3.bin piped to hashcat
|
||||
def hcatCombinator3(hcatHashType, hcatHashFile, wordlists):
|
||||
global hcatCombinator3Count
|
||||
global hcatProcess
|
||||
|
||||
if len(wordlists) < 3:
|
||||
print("[!] Combinator3 attack requires exactly 3 wordlists.")
|
||||
return
|
||||
|
||||
combinator3_bin = os.path.join(hate_path, "hashcat-utils/bin/combinator3.bin")
|
||||
with contextlib.ExitStack() as stack:
|
||||
resolved = [stack.enter_context(_wordlist_path(w)) for w in wordlists[:3]]
|
||||
generator_cmd = [combinator3_bin] + resolved
|
||||
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)
|
||||
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
|
||||
assert generator_proc.stdout is not None
|
||||
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()
|
||||
|
||||
hcatCombinator3Count = lineCount(hcatHashFile + ".out") - hcatHashCracked
|
||||
|
||||
|
||||
# CombinatorX Attack - N-way combination (2-8 wordlists) via combinatorX.bin piped to hashcat
|
||||
def hcatCombinatorX(hcatHashType, hcatHashFile, wordlists, separator=None):
|
||||
global hcatCombinatorXCount
|
||||
global hcatProcess
|
||||
|
||||
if len(wordlists) < 2:
|
||||
print("[!] CombinatorX attack requires at least 2 wordlists.")
|
||||
return
|
||||
|
||||
combinatorX_bin = os.path.join(hate_path, "hashcat-utils/bin/combinatorX.bin")
|
||||
with contextlib.ExitStack() as stack:
|
||||
resolved = [stack.enter_context(_wordlist_path(w)) for w in wordlists[:8]]
|
||||
generator_cmd = [combinatorX_bin]
|
||||
for i, f in enumerate(resolved, start=1):
|
||||
generator_cmd += [f"--file{i}", f]
|
||||
if separator:
|
||||
generator_cmd += ["--sepFill", separator]
|
||||
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)
|
||||
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
|
||||
assert generator_proc.stdout is not None
|
||||
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()
|
||||
|
||||
hcatCombinatorXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
|
||||
|
||||
|
||||
# NgramX Attack - n-gram candidates from corpus file piped to hashcat
|
||||
def hcatNgramX(hcatHashType, hcatHashFile, corpus, group_size=3):
|
||||
global hcatNgramXCount
|
||||
global hcatProcess
|
||||
|
||||
ngramX_bin = os.path.join(hate_path, "hashcat-utils/bin/ngramX.bin")
|
||||
with _wordlist_path(corpus) as resolved_corpus:
|
||||
generator_cmd = [ngramX_bin, resolved_corpus, str(group_size)]
|
||||
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)
|
||||
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
|
||||
assert generator_proc.stdout is not None
|
||||
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()
|
||||
|
||||
hcatNgramXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
|
||||
|
||||
|
||||
# Hybrid Attack
|
||||
def hcatHybrid(hcatHashType, hcatHashFile, wordlists=None):
|
||||
global hcatHybridCount
|
||||
@@ -2091,7 +2260,7 @@ def hcatMarkovTrain(source_file, hcatHashFile):
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(source_file, "rb") as stdin_f:
|
||||
with _open_wordlist(source_file) as stdin_f:
|
||||
hcatProcess = subprocess.Popen(
|
||||
[hcstat2gen_bin, hcstat2_path], stdin=stdin_f, stderr=subprocess.PIPE
|
||||
)
|
||||
@@ -2182,10 +2351,21 @@ 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)
|
||||
generator_cmd.append(wordlist_path)
|
||||
session_name = re.sub(
|
||||
r"[^a-zA-Z0-9_-]", "_", os.path.splitext(os.path.basename(hcatHashFile))[0]
|
||||
)
|
||||
@@ -2211,6 +2391,10 @@ def hcatCombipow(hcatHashType, hcatHashFile, wordlist, use_space_sep=True):
|
||||
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
|
||||
@@ -2248,7 +2432,7 @@ def hcatPrince(hcatHashType, hcatHashFile):
|
||||
hashcat_cmd.extend(shlex.split(hcatTuning))
|
||||
_append_potfile_arg(hashcat_cmd)
|
||||
hashcat_cmd = _add_debug_mode_for_rules(hashcat_cmd)
|
||||
with open(prince_base, "rb") as base:
|
||||
with _open_wordlist(prince_base) as base:
|
||||
prince_proc = subprocess.Popen(prince_cmd, stdin=base, stdout=subprocess.PIPE)
|
||||
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=prince_proc.stdout)
|
||||
prince_proc.stdout.close()
|
||||
@@ -2261,6 +2445,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(wordlist) 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/.
|
||||
@@ -2566,6 +2789,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")
|
||||
@@ -3340,6 +3608,22 @@ def middle_combinator():
|
||||
return _attacks.middle_combinator(_attack_ctx())
|
||||
|
||||
|
||||
def combinator3_crack():
|
||||
return _attacks.combinator3_crack(_attack_ctx())
|
||||
|
||||
|
||||
def combinatorX_crack():
|
||||
return _attacks.combinatorX_crack(_attack_ctx())
|
||||
|
||||
|
||||
def combinator_3plus_crack():
|
||||
return _attacks.combinator_3plus_crack(_attack_ctx())
|
||||
|
||||
|
||||
def ngram_attack():
|
||||
return _attacks.ngram_attack(_attack_ctx())
|
||||
|
||||
|
||||
def combinator_submenu():
|
||||
return _attacks.combinator_submenu(_attack_ctx())
|
||||
|
||||
@@ -3372,6 +3656,14 @@ def combipow_crack():
|
||||
return _attacks.combipow_crack(_attack_ctx())
|
||||
|
||||
|
||||
def generate_rules_crack():
|
||||
return _attacks.generate_rules_crack(_attack_ctx())
|
||||
|
||||
|
||||
def permute_crack():
|
||||
return _attacks.permute_crack(_attack_ctx())
|
||||
|
||||
|
||||
# convert hex words for recycling
|
||||
def convert_hex(working_file):
|
||||
processed_words = []
|
||||
@@ -3600,7 +3892,10 @@ def get_main_menu_items():
|
||||
("16", "OMEN Attack"),
|
||||
("17", "Ad-hoc Mask Attack"),
|
||||
("18", "Markov Brute Force Attack"),
|
||||
("21", "Combipow Passphrase Attack"),
|
||||
("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"),
|
||||
@@ -3638,7 +3933,10 @@ def get_main_menu_options():
|
||||
"16": omen_attack,
|
||||
"17": adhoc_mask_crack,
|
||||
"18": markov_brute_force,
|
||||
"21": combipow_crack,
|
||||
"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,
|
||||
|
||||
@@ -216,7 +216,7 @@ class TestCombinatorCrack:
|
||||
ctx._resolve_wordlist_path.assert_any_call("/wl/a.txt", ctx.hcatWordlists)
|
||||
ctx._resolve_wordlist_path.assert_any_call("/wl/b.txt", ctx.hcatWordlists)
|
||||
|
||||
def test_uses_only_first_two_wordlists(self) -> None:
|
||||
def test_three_wordlists_in_config_routes_to_combinator3(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatCombinationWordlist = ["/wl/a.txt", "/wl/b.txt", "/wl/c.txt"]
|
||||
ctx._resolve_wordlist_path.side_effect = lambda wl, _: wl
|
||||
@@ -224,9 +224,10 @@ class TestCombinatorCrack:
|
||||
with patch("builtins.input", return_value=""):
|
||||
combinator_crack(ctx)
|
||||
|
||||
call_wordlists = ctx.hcatCombination.call_args[0][2]
|
||||
assert len(call_wordlists) == 2
|
||||
assert "/wl/c.txt" not in call_wordlists
|
||||
ctx.hcatCombinator3.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
call_wordlists = ctx.hcatCombinator3.call_args[0][2]
|
||||
assert len(call_wordlists) == 3
|
||||
|
||||
|
||||
class TestHybridCrack:
|
||||
|
||||
154
tests/test_combinator3_combinatorX.py
Normal file
154
tests/test_combinator3_combinatorX.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from hate_crack.attacks import combinator_crack, combinator_submenu
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
return ctx
|
||||
|
||||
|
||||
class TestCombinatorCrackUnified:
|
||||
def test_two_wordlists_calls_hcatCombination(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", "", ""]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombination.assert_called_once()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
ctx.hcatCombinatorX.assert_not_called()
|
||||
|
||||
def test_three_wordlists_calls_hcatCombinator3(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt", "c.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", f"{tmp_path}/c.txt", "", ""]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombinator3.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
ctx.hcatCombinatorX.assert_not_called()
|
||||
|
||||
def test_four_wordlists_calls_hcatCombinatorX(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt", "c.txt", "d.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = [
|
||||
"n",
|
||||
f"{tmp_path}/a.txt",
|
||||
f"{tmp_path}/b.txt",
|
||||
f"{tmp_path}/c.txt",
|
||||
f"{tmp_path}/d.txt",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombinatorX.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
|
||||
def test_separator_forces_combinatorX_for_two_wordlists(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", "", "-"]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombinatorX.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
|
||||
def test_separator_forces_combinatorX_for_three_wordlists(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt", "c.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", f"{tmp_path}/c.txt", "", "-"]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombinatorX.assert_called_once()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
|
||||
def test_aborts_with_fewer_than_2_wordlists(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
(tmp_path / "a.txt").write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", ""]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
ctx.hcatCombinatorX.assert_not_called()
|
||||
|
||||
def test_aborts_when_no_wordlists_provided(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
with patch("builtins.input", side_effect=["n", ""]):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
ctx.hcatCombinatorX.assert_not_called()
|
||||
|
||||
def test_no_separator_passes_none_to_combinatorX(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt", "c.txt", "d.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = [
|
||||
"n",
|
||||
f"{tmp_path}/a.txt",
|
||||
f"{tmp_path}/b.txt",
|
||||
f"{tmp_path}/c.txt",
|
||||
f"{tmp_path}/d.txt",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
call_args = ctx.hcatCombinatorX.call_args
|
||||
positional_sep = call_args[0][3] if len(call_args[0]) >= 4 else None
|
||||
keyword_sep = call_args[1].get("separator")
|
||||
assert positional_sep in (None, "") and keyword_sep in (None, "")
|
||||
|
||||
|
||||
class TestCombinatorSubmenuUpdated:
|
||||
def test_submenu_option1_dispatches_to_combinator_crack(self):
|
||||
ctx = _make_ctx()
|
||||
with patch("hate_crack.attacks.combinator_crack") as mock_c, patch(
|
||||
"hate_crack.attacks.interactive_menu", side_effect=["1", "99"]
|
||||
):
|
||||
combinator_submenu(ctx)
|
||||
mock_c.assert_called_once_with(ctx)
|
||||
|
||||
def test_submenu_has_no_separate_3plus_option(self):
|
||||
"""Verify option 5 (3+) is removed - combinator is now unified under option 1."""
|
||||
ctx = _make_ctx()
|
||||
captured_items = []
|
||||
|
||||
def capture_menu(items, **kwargs):
|
||||
captured_items.extend(items)
|
||||
return "99"
|
||||
|
||||
with patch("hate_crack.attacks.interactive_menu", side_effect=capture_menu):
|
||||
combinator_submenu(ctx)
|
||||
|
||||
keys = [item[0] for item in captured_items]
|
||||
assert "1" in keys
|
||||
assert "5" not in keys
|
||||
assert "6" not in keys
|
||||
300
tests/test_combinator_wrappers.py
Normal file
300
tests/test_combinator_wrappers.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""Tests for hcatCombinator3 and hcatCombinatorX hashcat wrapper functions."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_mock_proc(wait_side_effect=None):
|
||||
proc = MagicMock()
|
||||
proc.stdout = 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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_module(hc_module):
|
||||
return hc_module._main
|
||||
|
||||
|
||||
class TestHcatCombinator3:
|
||||
def test_calls_combinator3_bin_with_three_files(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(3):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinator3" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinator3("1000", hash_file, wls)
|
||||
|
||||
calls = mock_popen.call_args_list
|
||||
assert len(calls) == 2
|
||||
combinator_cmd = calls[0][0][0]
|
||||
assert "combinator3" in combinator_cmd[0]
|
||||
assert wls[0] in combinator_cmd
|
||||
assert wls[1] in combinator_cmd
|
||||
assert wls[2] in combinator_cmd
|
||||
|
||||
def test_pipes_stdout_to_hashcat_stdin(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(3):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinator3" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinator3("1000", hash_file, wls)
|
||||
|
||||
calls = mock_popen.call_args_list
|
||||
hashcat_call_kwargs = calls[1][1]
|
||||
assert hashcat_call_kwargs.get("stdin") == combinator_proc.stdout
|
||||
|
||||
def test_aborts_with_fewer_than_3_wordlists(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl1 = str(tmp_path / "w1.txt")
|
||||
wl2 = str(tmp_path / "w2.txt")
|
||||
for p in [wl1, wl2]:
|
||||
open(p, "w").close()
|
||||
|
||||
with patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
main_module.hcatCombinator3("1000", hash_file, [wl1, wl2])
|
||||
|
||||
mock_popen.assert_not_called()
|
||||
|
||||
def test_keyboard_interrupt_kills_both_processes(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(3):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinator3" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect),
|
||||
):
|
||||
main_module.hcatCombinator3("1000", hash_file, wls)
|
||||
|
||||
hashcat_proc.kill.assert_called_once()
|
||||
combinator_proc.kill.assert_called_once()
|
||||
|
||||
|
||||
class TestHcatCombinatorX:
|
||||
def test_calls_combinatorX_bin_with_file_flags(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(2):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls)
|
||||
|
||||
calls = mock_popen.call_args_list
|
||||
assert len(calls) == 2
|
||||
combinator_cmd = calls[0][0][0]
|
||||
assert "combinatorX" in combinator_cmd[0]
|
||||
assert "--file1" in combinator_cmd
|
||||
assert "--file2" in combinator_cmd
|
||||
|
||||
def test_passes_sepfill_when_separator_given(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(2):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls, separator="-")
|
||||
|
||||
combinator_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert "--sepFill" in combinator_cmd
|
||||
sep_idx = combinator_cmd.index("--sepFill")
|
||||
assert combinator_cmd[sep_idx + 1] == "-"
|
||||
|
||||
def test_no_sepfill_when_separator_is_none(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(2):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls, separator=None)
|
||||
|
||||
combinator_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert "--sepFill" not in combinator_cmd
|
||||
|
||||
def test_aborts_with_fewer_than_2_wordlists(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl1 = str(tmp_path / "w1.txt")
|
||||
open(wl1, "w").close()
|
||||
|
||||
with patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
main_module.hcatCombinatorX("1000", hash_file, [wl1])
|
||||
|
||||
mock_popen.assert_not_called()
|
||||
|
||||
def test_supports_up_to_8_wordlists(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(8):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls)
|
||||
|
||||
combinator_cmd = mock_popen.call_args_list[0][0][0]
|
||||
for i in range(1, 9):
|
||||
assert f"--file{i}" in combinator_cmd
|
||||
|
||||
def test_keyboard_interrupt_kills_both_processes(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(2):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect),
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls)
|
||||
|
||||
hashcat_proc.kill.assert_called_once()
|
||||
combinator_proc.kill.assert_called_once()
|
||||
@@ -46,16 +46,16 @@ def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
|
||||
def test_combipow_crack_in_main_menu(cli):
|
||||
options = cli.get_main_menu_options()
|
||||
assert "21" in 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 "21" in keys
|
||||
assert "22" in keys
|
||||
labels = {k: label for k, label in items}
|
||||
assert "passphrase" in labels["21"].lower() or "combipow" in labels["21"].lower()
|
||||
assert "passphrase" in labels["22"].lower() or "combipow" in labels["22"].lower()
|
||||
|
||||
|
||||
# --- combipow_crack handler tests ---
|
||||
|
||||
@@ -122,6 +122,10 @@ def test_toggle_rule_parses_with_and_without_loopback(tmp_path: Path, capsys):
|
||||
pytest.skip("hashcat not available in PATH")
|
||||
if not _hashcat_sessions_writable():
|
||||
pytest.skip("hashcat session directory (~/.hashcat/sessions) is not writable")
|
||||
# Hashcat renames hashcat.induct after each run; recreate so loopback can write.
|
||||
(Path.home() / ".hashcat" / "sessions" / "hashcat.induct").mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
|
||||
show_output = os.environ.get("HATE_CRACK_SHOW_HASHCAT_OUTPUT") == "1"
|
||||
show_cmd = (
|
||||
|
||||
149
tests/test_ngram_gzip.py
Normal file
149
tests/test_ngram_gzip.py
Normal file
@@ -0,0 +1,149 @@
|
||||
import gzip
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from hate_crack.attacks import ngram_attack
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
return ctx
|
||||
|
||||
|
||||
class TestNgramAttack:
|
||||
def test_calls_hcatNgramX_with_corpus_and_group_size(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\nletmein\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="3"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(corpus), 3
|
||||
)
|
||||
|
||||
def test_default_group_size_is_3(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once()
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 3
|
||||
|
||||
def test_invalid_group_size_defaults_to_3(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="abc"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once()
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 3
|
||||
|
||||
def test_aborts_when_no_corpus_selected(self):
|
||||
ctx = _make_ctx()
|
||||
ctx.select_file_with_autocomplete.return_value = None
|
||||
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_not_called()
|
||||
|
||||
def test_custom_group_size_passed_through(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="5"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 5
|
||||
|
||||
|
||||
class TestIsGzipped:
|
||||
def test_detects_gzip_file(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
gz_file = tmp_path / "test.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\n")
|
||||
|
||||
assert _is_gzipped(str(gz_file)) is True
|
||||
|
||||
def test_plain_file_not_detected_as_gzip(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
plain = tmp_path / "test.txt"
|
||||
plain.write_bytes(b"password\n")
|
||||
|
||||
assert _is_gzipped(str(plain)) is False
|
||||
|
||||
def test_missing_file_returns_false(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
assert _is_gzipped(str(tmp_path / "nonexistent.txt")) is False
|
||||
|
||||
def test_empty_file_returns_false(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
empty = tmp_path / "empty.txt"
|
||||
empty.write_bytes(b"")
|
||||
|
||||
assert _is_gzipped(str(empty)) is False
|
||||
|
||||
|
||||
class TestWordlistPath:
|
||||
def test_plain_file_yields_original_path(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
plain = tmp_path / "words.txt"
|
||||
plain.write_text("password\n")
|
||||
|
||||
with _wordlist_path(str(plain)) as result:
|
||||
assert result == str(plain)
|
||||
|
||||
def test_gzip_file_yields_temp_file_with_content(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
gz_file = tmp_path / "words.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\nletmein\n")
|
||||
|
||||
with _wordlist_path(str(gz_file)) as result:
|
||||
assert result != str(gz_file)
|
||||
assert os.path.isfile(result)
|
||||
with open(result, "rb") as f:
|
||||
assert f.read() == b"password\nletmein\n"
|
||||
|
||||
def test_gzip_temp_file_removed_after_context(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
gz_file = tmp_path / "words.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\n")
|
||||
|
||||
with _wordlist_path(str(gz_file)) as result:
|
||||
tmp_path_used = result
|
||||
|
||||
assert not os.path.exists(tmp_path_used)
|
||||
|
||||
def test_plain_file_not_deleted_after_context(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
plain = tmp_path / "words.txt"
|
||||
plain.write_text("password\n")
|
||||
|
||||
with _wordlist_path(str(plain)) as result:
|
||||
assert result == str(plain)
|
||||
|
||||
assert plain.exists()
|
||||
67
tests/test_permute_attack.py
Normal file
67
tests/test_permute_attack.py
Normal file
@@ -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()
|
||||
162
tests/test_permute_wrapper.py
Normal file
162
tests/test_permute_wrapper.py
Normal file
@@ -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()
|
||||
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
|
||||
@@ -26,7 +26,10 @@ 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"),
|
||||
("21", CLI_MODULE._attacks, "combipow_crack", "combipow"),
|
||||
("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"),
|
||||
|
||||
@@ -40,7 +40,7 @@ def main():
|
||||
|
||||
# Resolve binary paths relative to script location
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
ext = ".app" if sys.platform == "darwin" else ".bin"
|
||||
ext = ".bin" 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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user