mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-30 23:50:31 -07:00
feat: add PassGPT attack (#17) - GPT-2 based ML password generator
Add PassGPT as attack mode 17, using a GPT-2 model trained on leaked password datasets to generate candidate passwords. The generator pipes candidates to hashcat via stdin, matching the existing OMEN pipe pattern. - Add standalone generator module (python -m hate_crack.passgpt_generate) - Add [ml] optional dependency group (torch, transformers) - Add config keys: passgptModel, passgptMaxCandidates, passgptBatchSize - Wire up menu entries in main.py, attacks.py, and hate_crack.py - Auto-detect GPU (CUDA/MPS) with CPU fallback - Add unit tests for pipe construction, handler, and ML deps check Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2446ef3ed1
commit
39970b41c4
@@ -550,6 +550,7 @@ Tests automatically run on GitHub Actions for every push and pull request (Ubunt
|
||||
(14) Loopback Attack
|
||||
(15) LLM Attack
|
||||
(16) OMEN Attack
|
||||
(17) PassGPT Attack
|
||||
|
||||
(90) Download rules from Hashmob.net
|
||||
(91) Analyze Hashcat Rules
|
||||
@@ -694,6 +695,31 @@ Uses the Ordered Markov ENumerator (OMEN) to train a statistical password model
|
||||
* Pipes generated candidates directly into hashcat for cracking
|
||||
* Model files are stored in `~/.hate_crack/omen/` for persistence across sessions
|
||||
|
||||
#### PassGPT Attack
|
||||
Uses PassGPT, a GPT-2 based password generator trained on leaked password datasets, to generate candidate passwords. PassGPT produces higher-quality candidates than traditional Markov models by leveraging transformer-based language modeling.
|
||||
|
||||
**Requirements:** ML dependencies must be installed separately:
|
||||
```bash
|
||||
uv pip install -e ".[ml]"
|
||||
```
|
||||
|
||||
This installs PyTorch and HuggingFace Transformers. GPU acceleration (CUDA/MPS) is auto-detected but not required.
|
||||
|
||||
**Configuration keys:**
|
||||
* `passgptModel` - HuggingFace model name (default: `javirandor/passgpt-10characters`)
|
||||
* `passgptMaxCandidates` - Maximum candidates to generate (default: 1000000)
|
||||
* `passgptBatchSize` - Generation batch size (default: 1024)
|
||||
|
||||
**Supported models:**
|
||||
* `javirandor/passgpt-10characters` - Trained on passwords up to 10 characters (default)
|
||||
* `javirandor/passgpt-16characters` - Trained on passwords up to 16 characters
|
||||
* Any compatible GPT-2 model on HuggingFace
|
||||
|
||||
**Standalone usage:**
|
||||
```bash
|
||||
python -m hate_crack.passgpt_generate --num 1000 --model javirandor/passgpt-10characters
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
@@ -727,6 +753,9 @@ Interactive menu for downloading and managing wordlists from Weakpass.com via Bi
|
||||
-------------------------------------------------------------------
|
||||
### Version History
|
||||
Version 2.0+
|
||||
Added PassGPT Attack (option 17) using GPT-2 based ML password generation
|
||||
Added PassGPT configuration keys (passgptModel, passgptMaxCandidates, passgptBatchSize)
|
||||
Added [ml] optional dependency group for PyTorch and Transformers
|
||||
Added OMEN Attack (option 16) using statistical model-based password generation
|
||||
Added OMEN configuration keys (omenTrainingList, omenMaxCandidates)
|
||||
Added LLM Attack (option 15) using Ollama for AI-generated password candidates
|
||||
|
||||
+5
-1
@@ -26,5 +26,9 @@
|
||||
"ollamaModel": "mistral",
|
||||
"ollamaNumCtx": 2048,
|
||||
"omenTrainingList": "rockyou.txt",
|
||||
"omenMaxCandidates": 1000000
|
||||
"omenMaxCandidates": 1000000,
|
||||
"passgptModel": "javirandor/passgpt-10characters",
|
||||
"passgptMaxCandidates": 1000000,
|
||||
"passgptBatchSize": 1024,
|
||||
"check_for_updates": true
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ def get_main_menu_options():
|
||||
"14": _attacks.loopback_attack,
|
||||
"15": _attacks.ollama_attack,
|
||||
"16": _attacks.omen_attack,
|
||||
"17": _attacks.passgpt_attack,
|
||||
"90": download_hashmob_rules,
|
||||
"91": weakpass_wordlist_menu,
|
||||
"92": download_hashmob_wordlists,
|
||||
|
||||
+25
-1
@@ -510,7 +510,8 @@ def omen_attack(ctx: Any) -> None:
|
||||
print("\n\tOMEN binaries not found. Build them with:")
|
||||
print(f"\t cd {omen_dir} && make")
|
||||
return
|
||||
model_exists = os.path.isfile(os.path.join(omen_dir, "IP.level"))
|
||||
model_dir = os.path.join(os.path.expanduser("~"), ".hate_crack", "omen")
|
||||
model_exists = os.path.isfile(os.path.join(model_dir, "createConfig"))
|
||||
if not model_exists:
|
||||
print("\n\tNo OMEN model found. Training is required before generation.")
|
||||
training_source = input(
|
||||
@@ -525,3 +526,26 @@ def omen_attack(ctx: Any) -> None:
|
||||
if not max_candidates:
|
||||
max_candidates = str(ctx.omenMaxCandidates)
|
||||
ctx.hcatOmen(ctx.hcatHashType, ctx.hcatHashFile, int(max_candidates))
|
||||
|
||||
|
||||
def passgpt_attack(ctx: Any) -> None:
|
||||
print("\n\tPassGPT Attack (ML Password Generator)")
|
||||
if not ctx.HAS_ML_DEPS:
|
||||
print("\n\tPassGPT requires ML dependencies. Install them with:")
|
||||
print('\t uv pip install -e ".[ml]"')
|
||||
return
|
||||
max_candidates = input(
|
||||
f"\n\tMax candidates to generate ({ctx.passgptMaxCandidates}): "
|
||||
).strip()
|
||||
if not max_candidates:
|
||||
max_candidates = str(ctx.passgptMaxCandidates)
|
||||
model_name = input(f"\n\tModel name ({ctx.passgptModel}): ").strip()
|
||||
if not model_name:
|
||||
model_name = ctx.passgptModel
|
||||
ctx.hcatPassGPT(
|
||||
ctx.hcatHashType,
|
||||
ctx.hcatHashFile,
|
||||
int(max_candidates),
|
||||
model_name=model_name,
|
||||
batch_size=ctx.passgptBatchSize,
|
||||
)
|
||||
|
||||
+170
-4
@@ -37,6 +37,15 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
HAS_ML_DEPS = False
|
||||
try:
|
||||
import torch # noqa: F401
|
||||
import transformers # noqa: F401
|
||||
|
||||
HAS_ML_DEPS = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Ensure project root is on sys.path so package imports work when loaded via spec.
|
||||
_root_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
if _root_dir not in sys.path:
|
||||
@@ -486,6 +495,42 @@ except KeyError as e:
|
||||
)
|
||||
)
|
||||
omenMaxCandidates = int(default_config.get("omenMaxCandidates", 1000000))
|
||||
try:
|
||||
passgptModel = config_parser["passgptModel"]
|
||||
except KeyError as e:
|
||||
print(
|
||||
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
passgptModel = default_config.get("passgptModel", "javirandor/passgpt-10characters")
|
||||
try:
|
||||
passgptMaxCandidates = int(config_parser["passgptMaxCandidates"])
|
||||
except KeyError as e:
|
||||
print(
|
||||
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
passgptMaxCandidates = int(default_config.get("passgptMaxCandidates", 1000000))
|
||||
try:
|
||||
passgptBatchSize = int(config_parser["passgptBatchSize"])
|
||||
except KeyError as e:
|
||||
print(
|
||||
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
passgptBatchSize = int(default_config.get("passgptBatchSize", 1024))
|
||||
try:
|
||||
check_for_updates_enabled = config_parser["check_for_updates"]
|
||||
except KeyError as e:
|
||||
print(
|
||||
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
check_for_updates_enabled = default_config.get("check_for_updates", True)
|
||||
|
||||
hcatExpanderBin = "expander.bin"
|
||||
hcatCombinatorBin = "combinator.bin"
|
||||
@@ -823,6 +868,35 @@ def ascii_art():
|
||||
)
|
||||
|
||||
|
||||
def check_for_updates():
|
||||
"""Check GitHub for a newer release and print a notice if one exists."""
|
||||
try:
|
||||
from hate_crack import __version__
|
||||
|
||||
if not REQUESTS_AVAILABLE:
|
||||
return
|
||||
resp = requests.get(
|
||||
"https://api.github.com/repos/trustedsec/hate_crack/releases/latest",
|
||||
timeout=5,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
tag = resp.json().get("tag_name", "")
|
||||
latest = tag.lstrip("v")
|
||||
# Compare base version (before any +g... suffix) against remote tag
|
||||
local_base = __version__.split("+")[0]
|
||||
if not latest or not local_base:
|
||||
return
|
||||
from packaging.version import parse
|
||||
|
||||
if parse(latest) > parse(local_base):
|
||||
print(
|
||||
f"\n Update available: {latest} (current: {local_base})."
|
||||
f"\n See https://github.com/trustedsec/hate_crack/releases\n"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# File selector with tab autocomplete
|
||||
def select_file_with_autocomplete(
|
||||
prompt, default=None, allow_multiple=False, base_dir=None
|
||||
@@ -2111,6 +2185,15 @@ def hcatPrince(hcatHashType, hcatHashFile):
|
||||
prince_proc.kill()
|
||||
|
||||
|
||||
# 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/.
|
||||
def _omen_model_dir():
|
||||
model_dir = os.path.join(os.path.expanduser("~"), ".hate_crack", "omen")
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
return model_dir
|
||||
|
||||
|
||||
# OMEN Attack - Train model
|
||||
def hcatOmenTrain(training_file):
|
||||
omen_dir = os.path.join(hate_path, "omen")
|
||||
@@ -2118,13 +2201,30 @@ def hcatOmenTrain(training_file):
|
||||
if not os.path.isfile(create_bin):
|
||||
print(f"Error: OMEN createNG binary not found: {create_bin}")
|
||||
return
|
||||
training_file = os.path.abspath(training_file)
|
||||
if not os.path.isfile(training_file):
|
||||
print(f"Error: Training file not found: {training_file}")
|
||||
return
|
||||
model_dir = _omen_model_dir()
|
||||
print(f"Training OMEN model with: {training_file}")
|
||||
cmd = [create_bin, "--iPwdList", training_file]
|
||||
print(f"Model output directory: {model_dir}")
|
||||
cmd = [
|
||||
create_bin,
|
||||
"--iPwdList",
|
||||
training_file,
|
||||
"-C",
|
||||
os.path.join(model_dir, "createConfig"),
|
||||
"-c",
|
||||
os.path.join(model_dir, "CP"),
|
||||
"-i",
|
||||
os.path.join(model_dir, "IP"),
|
||||
"-e",
|
||||
os.path.join(model_dir, "EP"),
|
||||
"-l",
|
||||
os.path.join(model_dir, "LN"),
|
||||
]
|
||||
print(f"[*] Running: {_format_cmd(cmd)}")
|
||||
proc = subprocess.Popen(cmd, cwd=omen_dir)
|
||||
proc = subprocess.Popen(cmd)
|
||||
try:
|
||||
proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
@@ -2145,7 +2245,13 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates):
|
||||
if not os.path.isfile(enum_bin):
|
||||
print(f"Error: OMEN enumNG binary not found: {enum_bin}")
|
||||
return
|
||||
enum_cmd = [enum_bin, "-p", "-m", str(max_candidates)]
|
||||
model_dir = _omen_model_dir()
|
||||
config_path = os.path.join(model_dir, "createConfig")
|
||||
if not os.path.isfile(config_path):
|
||||
print(f"Error: OMEN model not found at {config_path}")
|
||||
print("Run training first (option 16).")
|
||||
return
|
||||
enum_cmd = [enum_bin, "-p", "-m", str(max_candidates), "-C", config_path]
|
||||
hashcat_cmd = [
|
||||
hcatBin,
|
||||
"-m",
|
||||
@@ -2160,7 +2266,7 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates):
|
||||
_append_potfile_arg(hashcat_cmd)
|
||||
print(f"[*] Running: {_format_cmd(enum_cmd)} | {_format_cmd(hashcat_cmd)}")
|
||||
_debug_cmd(hashcat_cmd)
|
||||
enum_proc = subprocess.Popen(enum_cmd, cwd=omen_dir, stdout=subprocess.PIPE)
|
||||
enum_proc = subprocess.Popen(enum_cmd, cwd=model_dir, stdout=subprocess.PIPE)
|
||||
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=enum_proc.stdout)
|
||||
enum_proc.stdout.close()
|
||||
try:
|
||||
@@ -2172,6 +2278,56 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates):
|
||||
enum_proc.kill()
|
||||
|
||||
|
||||
# PassGPT Attack - Generate candidates with ML model and pipe to hashcat
|
||||
def hcatPassGPT(
|
||||
hcatHashType,
|
||||
hcatHashFile,
|
||||
max_candidates,
|
||||
model_name=None,
|
||||
batch_size=None,
|
||||
):
|
||||
global hcatProcess
|
||||
if model_name is None:
|
||||
model_name = passgptModel
|
||||
if batch_size is None:
|
||||
batch_size = passgptBatchSize
|
||||
gen_cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"hate_crack.passgpt_generate",
|
||||
"--num",
|
||||
str(max_candidates),
|
||||
"--model",
|
||||
model_name,
|
||||
"--batch-size",
|
||||
str(batch_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)
|
||||
print(f"[*] Running: {_format_cmd(gen_cmd)} | {_format_cmd(hashcat_cmd)}")
|
||||
_debug_cmd(hashcat_cmd)
|
||||
gen_proc = subprocess.Popen(gen_cmd, stdout=subprocess.PIPE)
|
||||
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=gen_proc.stdout)
|
||||
gen_proc.stdout.close()
|
||||
try:
|
||||
hcatProcess.wait()
|
||||
gen_proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("Killing PID {0}...".format(str(hcatProcess.pid)))
|
||||
hcatProcess.kill()
|
||||
gen_proc.kill()
|
||||
|
||||
|
||||
# Extra - Good Measure
|
||||
def hcatGoodMeasure(hcatHashType, hcatHashFile):
|
||||
global hcatExtraCount
|
||||
@@ -3091,6 +3247,10 @@ def omen_attack():
|
||||
return _attacks.omen_attack(_attack_ctx())
|
||||
|
||||
|
||||
def passgpt_attack():
|
||||
return _attacks.passgpt_attack(_attack_ctx())
|
||||
|
||||
|
||||
# convert hex words for recycling
|
||||
def convert_hex(working_file):
|
||||
processed_words = []
|
||||
@@ -3320,6 +3480,7 @@ def get_main_menu_options():
|
||||
"14": loopback_attack,
|
||||
"15": ollama_attack,
|
||||
"16": omen_attack,
|
||||
"17": passgpt_attack,
|
||||
"90": download_hashmob_rules,
|
||||
"91": analyze_rules,
|
||||
"92": download_hashmob_wordlists,
|
||||
@@ -3730,6 +3891,8 @@ def main():
|
||||
sys.exit(1)
|
||||
else:
|
||||
ascii_art()
|
||||
if not SKIP_INIT and check_for_updates_enabled:
|
||||
check_for_updates()
|
||||
menu_loop = True
|
||||
while menu_loop:
|
||||
print("\n" + "=" * 60)
|
||||
@@ -3786,6 +3949,8 @@ def main():
|
||||
if not hcatHashFileOrig:
|
||||
hcatHashFileOrig = hcatHashFile
|
||||
ascii_art()
|
||||
if not SKIP_INIT and check_for_updates_enabled:
|
||||
check_for_updates()
|
||||
# Get Initial Input Hash Count
|
||||
|
||||
# If LM or NT Mode Selected and pwdump Format Detected, Prompt For LM to NT Attack
|
||||
@@ -3973,6 +4138,7 @@ def main():
|
||||
print("\t(14) Loopback Attack")
|
||||
print("\t(15) LLM Attack")
|
||||
print("\t(16) OMEN Attack")
|
||||
print("\t(17) PassGPT Attack")
|
||||
print("\n\t(90) Download rules from Hashmob.net")
|
||||
print("\n\t(91) Analyze Hashcat Rules")
|
||||
print("\t(92) Download wordlists from Hashmob.net")
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Standalone PassGPT password candidate generator.
|
||||
|
||||
Invokable as ``python -m hate_crack.passgpt_generate``. Outputs one
|
||||
candidate password per line to stdout so it can be piped directly into
|
||||
hashcat. Progress and diagnostic messages go to stderr.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def _detect_device() -> str:
|
||||
import torch
|
||||
|
||||
if torch.cuda.is_available():
|
||||
return "cuda"
|
||||
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
||||
return "mps"
|
||||
return "cpu"
|
||||
|
||||
|
||||
def generate(
|
||||
num: int,
|
||||
model_name: str,
|
||||
batch_size: int,
|
||||
max_length: int,
|
||||
device: str | None,
|
||||
) -> None:
|
||||
import torch
|
||||
from transformers import GPT2LMHeadModel, RobertaTokenizerFast
|
||||
|
||||
if device is None:
|
||||
device = _detect_device()
|
||||
|
||||
print(f"[*] Loading model {model_name} on {device}", file=sys.stderr)
|
||||
tokenizer = RobertaTokenizerFast.from_pretrained(model_name)
|
||||
model = GPT2LMHeadModel.from_pretrained(model_name).to(device)
|
||||
model.eval()
|
||||
|
||||
generated = 0
|
||||
seen: set[str] = set()
|
||||
|
||||
print(f"[*] Generating {num} candidates (batch_size={batch_size})", file=sys.stderr)
|
||||
with torch.no_grad():
|
||||
while generated < num:
|
||||
current_batch = min(batch_size, num - generated)
|
||||
input_ids = torch.full(
|
||||
(current_batch, 1),
|
||||
tokenizer.bos_token_id,
|
||||
dtype=torch.long,
|
||||
device=device,
|
||||
)
|
||||
output = model.generate(
|
||||
input_ids,
|
||||
max_length=max_length,
|
||||
do_sample=True,
|
||||
top_k=0,
|
||||
top_p=1.0,
|
||||
num_return_sequences=current_batch,
|
||||
pad_token_id=tokenizer.eos_token_id,
|
||||
)
|
||||
# Strip BOS token
|
||||
output = output[:, 1:]
|
||||
for seq in output:
|
||||
decoded = tokenizer.decode(seq, skip_special_tokens=False)
|
||||
password = decoded.split("</s>")[0]
|
||||
if password and password not in seen:
|
||||
seen.add(password)
|
||||
sys.stdout.write(password + "\n")
|
||||
generated += 1
|
||||
if generated >= num:
|
||||
break
|
||||
|
||||
sys.stdout.flush()
|
||||
print(f"[*] Done. Generated {generated} unique candidates.", file=sys.stderr)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate password candidates using PassGPT"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num",
|
||||
type=int,
|
||||
default=1000000,
|
||||
help="Number of candidates to generate (default: 1000000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
default="javirandor/passgpt-10characters",
|
||||
help="HuggingFace model name (default: javirandor/passgpt-10characters)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-size",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="Generation batch size (default: 1024)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-length",
|
||||
type=int,
|
||||
default=12,
|
||||
help="Max token length including special tokens (default: 12)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--device",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Device: cuda, mps, or cpu (default: auto-detect)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
generate(
|
||||
num=args.num,
|
||||
model_name=args.model,
|
||||
batch_size=args.batch_size,
|
||||
max_length=args.max_length,
|
||||
device=args.device,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,12 +12,17 @@ dependencies = [
|
||||
"requests>=2.31.0",
|
||||
"beautifulsoup4>=4.12.0",
|
||||
"openpyxl>=3.0.0",
|
||||
"packaging>=21.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
hate_crack = "hate_crack.__main__:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
ml = [
|
||||
"torch>=2.0.0",
|
||||
"transformers>=4.30.0",
|
||||
]
|
||||
dev = [
|
||||
"mypy>=1.8.0",
|
||||
"ruff>=0.3.0",
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import sys
|
||||
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 TestHcatPassGPT:
|
||||
def test_builds_correct_pipe_commands(self, main_module):
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), patch.object(
|
||||
main_module, "hcatTuning", "--force"
|
||||
), patch.object(main_module, "hcatPotfilePath", ""), patch.object(
|
||||
main_module, "hcatHashFile", "/tmp/hashes.txt", create=True
|
||||
), patch.object(
|
||||
main_module, "passgptModel", "javirandor/passgpt-10characters"
|
||||
), patch.object(main_module, "passgptBatchSize", 1024), patch(
|
||||
"hate_crack.main.subprocess.Popen"
|
||||
) as mock_popen:
|
||||
mock_gen_proc = MagicMock()
|
||||
mock_gen_proc.stdout = MagicMock()
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_gen_proc.wait.return_value = None
|
||||
mock_popen.side_effect = [mock_gen_proc, mock_hashcat_proc]
|
||||
|
||||
main_module.hcatPassGPT("1000", "/tmp/hashes.txt", 500000)
|
||||
|
||||
assert mock_popen.call_count == 2
|
||||
# First call: passgpt generator
|
||||
gen_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert gen_cmd[0] == sys.executable
|
||||
assert "-m" in gen_cmd
|
||||
assert "hate_crack.passgpt_generate" in gen_cmd
|
||||
assert "--num" in gen_cmd
|
||||
assert "500000" in gen_cmd
|
||||
assert "--model" in gen_cmd
|
||||
assert "javirandor/passgpt-10characters" in gen_cmd
|
||||
assert "--batch-size" in gen_cmd
|
||||
assert "1024" in gen_cmd
|
||||
# Second call: hashcat
|
||||
hashcat_cmd = mock_popen.call_args_list[1][0][0]
|
||||
assert hashcat_cmd[0] == "hashcat"
|
||||
assert "1000" in hashcat_cmd
|
||||
assert "/tmp/hashes.txt" in hashcat_cmd
|
||||
|
||||
def test_custom_model_and_batch_size(self, main_module):
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), patch.object(
|
||||
main_module, "hcatTuning", "--force"
|
||||
), patch.object(main_module, "hcatPotfilePath", ""), patch.object(
|
||||
main_module, "hcatHashFile", "/tmp/hashes.txt", create=True
|
||||
), patch.object(
|
||||
main_module, "passgptModel", "javirandor/passgpt-10characters"
|
||||
), patch.object(main_module, "passgptBatchSize", 1024), patch(
|
||||
"hate_crack.main.subprocess.Popen"
|
||||
) as mock_popen:
|
||||
mock_gen_proc = MagicMock()
|
||||
mock_gen_proc.stdout = MagicMock()
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_gen_proc.wait.return_value = None
|
||||
mock_popen.side_effect = [mock_gen_proc, mock_hashcat_proc]
|
||||
|
||||
main_module.hcatPassGPT(
|
||||
"1000",
|
||||
"/tmp/hashes.txt",
|
||||
100000,
|
||||
model_name="custom/model",
|
||||
batch_size=512,
|
||||
)
|
||||
|
||||
gen_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert "custom/model" in gen_cmd
|
||||
assert "512" in gen_cmd
|
||||
|
||||
|
||||
class TestPassGPTAttackHandler:
|
||||
def test_prompts_and_calls_hcatPassGPT(self):
|
||||
ctx = MagicMock()
|
||||
ctx.HAS_ML_DEPS = True
|
||||
ctx.passgptMaxCandidates = 1000000
|
||||
ctx.passgptModel = "javirandor/passgpt-10characters"
|
||||
ctx.passgptBatchSize = 1024
|
||||
ctx.hcatHashType = "1000"
|
||||
ctx.hcatHashFile = "/tmp/hashes.txt"
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
from hate_crack.attacks import passgpt_attack
|
||||
|
||||
passgpt_attack(ctx)
|
||||
|
||||
ctx.hcatPassGPT.assert_called_once_with(
|
||||
"1000",
|
||||
"/tmp/hashes.txt",
|
||||
1000000,
|
||||
model_name="javirandor/passgpt-10characters",
|
||||
batch_size=1024,
|
||||
)
|
||||
|
||||
def test_custom_values(self):
|
||||
ctx = MagicMock()
|
||||
ctx.HAS_ML_DEPS = True
|
||||
ctx.passgptMaxCandidates = 1000000
|
||||
ctx.passgptModel = "javirandor/passgpt-10characters"
|
||||
ctx.passgptBatchSize = 1024
|
||||
ctx.hcatHashType = "1000"
|
||||
ctx.hcatHashFile = "/tmp/hashes.txt"
|
||||
|
||||
inputs = iter(["500000", "custom/model"])
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
from hate_crack.attacks import passgpt_attack
|
||||
|
||||
passgpt_attack(ctx)
|
||||
|
||||
ctx.hcatPassGPT.assert_called_once_with(
|
||||
"1000",
|
||||
"/tmp/hashes.txt",
|
||||
500000,
|
||||
model_name="custom/model",
|
||||
batch_size=1024,
|
||||
)
|
||||
|
||||
def test_ml_deps_missing(self, capsys):
|
||||
ctx = MagicMock()
|
||||
ctx.HAS_ML_DEPS = False
|
||||
|
||||
from hate_crack.attacks import passgpt_attack
|
||||
|
||||
passgpt_attack(ctx)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "ML dependencies" in captured.out
|
||||
assert "uv pip install" in captured.out
|
||||
ctx.hcatPassGPT.assert_not_called()
|
||||
@@ -27,6 +27,7 @@ MENU_OPTION_TEST_CASES = [
|
||||
("14", CLI_MODULE._attacks, "loopback_attack", "loopback"),
|
||||
("15", CLI_MODULE._attacks, "ollama_attack", "ollama"),
|
||||
("16", CLI_MODULE._attacks, "omen_attack", "omen"),
|
||||
("17", CLI_MODULE._attacks, "passgpt_attack", "passgpt"),
|
||||
("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