mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-04-28 12:03:11 -07:00
Verifies that a pwdump file at /tmp/test_hashes.ntds produces output at /tmp/test_hashes.ntds.out using real hashcat. Confirms no files leak into the project directory. Gated behind HATE_CRACK_RUN_E2E=1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
194 lines
6.8 KiB
Python
194 lines
6.8 KiB
Python
"""E2E test: output files land next to the hashfile, not in the package directory.
|
|
|
|
Requires: hashcat installed, HATE_CRACK_RUN_E2E=1
|
|
|
|
Creates a dummy pwdump file at /tmp/test_hashes.ntds with known weak
|
|
NTLM hashes, runs a real hashcat dictionary attack, then verifies:
|
|
- /tmp/test_hashes.ntds.nt (NT hash extraction)
|
|
- /tmp/test_hashes.ntds.nt.out (hashcat cracked output)
|
|
- /tmp/test_hashes.ntds.out (combined pwdump + password)
|
|
- No symlinks or output files created in the project directory
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
# Known NTLM hashes for weak passwords
|
|
PWDUMP_LINES = [
|
|
"alice:500:aad3b435b51404eeaad3b435b51404ee:8846f7eaee8fb117ad06bdd830b7586c:::\n",
|
|
"bob:501:aad3b435b51404eeaad3b435b51404ee:32ed87bdb5fdc5e9cba88547376818d4:::\n",
|
|
]
|
|
WORDLIST = ["password\n", "123456\n"]
|
|
EXPECTED_CRACKS = {
|
|
"8846f7eaee8fb117ad06bdd830b7586c": "password",
|
|
"32ed87bdb5fdc5e9cba88547376818d4": "123456",
|
|
}
|
|
|
|
HASH_BASE = Path("/tmp/test_hashes.ntds")
|
|
TEST_FILES = [
|
|
HASH_BASE,
|
|
Path(f"{HASH_BASE}.nt"),
|
|
Path(f"{HASH_BASE}.nt.out"),
|
|
Path(f"{HASH_BASE}.out"),
|
|
Path(f"{HASH_BASE}.lm"),
|
|
Path(f"{HASH_BASE}.lm.cracked"),
|
|
Path(f"{HASH_BASE}.working"),
|
|
Path(f"{HASH_BASE}.masks"),
|
|
Path(f"{HASH_BASE}.expanded"),
|
|
Path(f"{HASH_BASE}.combined"),
|
|
Path(f"{HASH_BASE}.passwords"),
|
|
Path("/tmp/test_wordlist.txt"),
|
|
Path("/tmp/test_potfile.pot"),
|
|
]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_test_files():
|
|
"""Remove all test artifacts before and after each test."""
|
|
for f in TEST_FILES:
|
|
f.unlink(missing_ok=True)
|
|
yield
|
|
for f in TEST_FILES:
|
|
f.unlink(missing_ok=True)
|
|
|
|
|
|
def _hashcat_available() -> bool:
|
|
return shutil.which("hashcat") is not None
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("HATE_CRACK_RUN_E2E") != "1",
|
|
reason="Set HATE_CRACK_RUN_E2E=1 to run e2e tests.",
|
|
)
|
|
@pytest.mark.skipif(not _hashcat_available(), reason="hashcat not installed")
|
|
class TestOutputPathE2E:
|
|
"""Verify output files land next to the hashfile in /tmp/, not in the project dir."""
|
|
|
|
def _create_hash_file(self):
|
|
HASH_BASE.write_text("".join(PWDUMP_LINES))
|
|
|
|
def _create_wordlist(self):
|
|
Path("/tmp/test_wordlist.txt").write_text("".join(WORDLIST))
|
|
|
|
def _extract_nt_hashes(self):
|
|
"""Extract NT hashes (field 4) from pwdump, sorted unique — mirrors main.py preprocessing."""
|
|
nt_hashes = set()
|
|
with open(HASH_BASE) as f:
|
|
for line in f:
|
|
parts = line.strip().split(":")
|
|
if len(parts) >= 4:
|
|
nt_hashes.add(parts[3])
|
|
nt_path = Path(f"{HASH_BASE}.nt")
|
|
nt_path.write_text("\n".join(sorted(nt_hashes)) + "\n")
|
|
return str(nt_path)
|
|
|
|
def _run_hashcat_crack(self, nt_file: str):
|
|
"""Run a real hashcat dictionary attack against extracted NT hashes."""
|
|
potfile = "/tmp/test_potfile.pot"
|
|
out_file = f"{nt_file}.out"
|
|
cmd = [
|
|
"hashcat",
|
|
"-m", "1000",
|
|
nt_file,
|
|
"/tmp/test_wordlist.txt",
|
|
"-a", "0",
|
|
"-o", out_file,
|
|
f"--potfile-path={potfile}",
|
|
"--potfile-disable",
|
|
"--quiet",
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
# hashcat returns 0 (cracked) or 1 (exhausted) on success
|
|
assert result.returncode in (0, 1), (
|
|
f"hashcat failed (rc={result.returncode}): {result.stderr}"
|
|
)
|
|
return out_file
|
|
|
|
def _combine_ntlm_output(self, nt_out_file: str):
|
|
"""Combine cracked NT hashes back into pwdump format — mirrors combine_ntlm_output()."""
|
|
hashes = {}
|
|
if os.path.isfile(nt_out_file):
|
|
with open(nt_out_file) as f:
|
|
for line in f:
|
|
parts = line.strip().split(":", 1)
|
|
if len(parts) == 2:
|
|
hashes[parts[0]] = parts[1]
|
|
|
|
if not hashes:
|
|
return
|
|
|
|
combined_path = f"{HASH_BASE}.out"
|
|
with open(combined_path, "w") as out, open(HASH_BASE) as orig:
|
|
for line in orig:
|
|
parts = line.split(":")
|
|
if len(parts) >= 4 and parts[3] in hashes:
|
|
out.write(line.strip() + hashes[parts[3]] + "\n")
|
|
|
|
def test_output_files_land_in_tmp(self):
|
|
"""Full flow: pwdump in /tmp -> hashcat -> combined output in /tmp."""
|
|
self._create_hash_file()
|
|
self._create_wordlist()
|
|
|
|
# Step 1: Extract NT hashes (preprocessing)
|
|
nt_file = self._extract_nt_hashes()
|
|
assert os.path.isfile(nt_file), f"NT extraction failed: {nt_file}"
|
|
|
|
# Step 2: Crack with real hashcat
|
|
nt_out = self._run_hashcat_crack(nt_file)
|
|
assert os.path.isfile(nt_out), f"hashcat output missing: {nt_out}"
|
|
|
|
# Step 3: Combine back to pwdump format
|
|
self._combine_ntlm_output(nt_out)
|
|
|
|
# Verify: combined output exists at /tmp/test_hashes.ntds.out
|
|
combined = Path(f"{HASH_BASE}.out")
|
|
assert combined.exists(), f"Combined output missing: {combined}"
|
|
assert combined.stat().st_size > 0, "Combined output is empty"
|
|
|
|
# Verify: combined output has correct pwdump + password format
|
|
with open(combined) as f:
|
|
lines = f.readlines()
|
|
assert len(lines) == 2, f"Expected 2 cracked lines, got {len(lines)}"
|
|
for line in lines:
|
|
parts = line.strip().split(":")
|
|
assert len(parts) >= 4, f"Malformed combined line: {line}"
|
|
nt_hash = parts[3]
|
|
assert nt_hash in EXPECTED_CRACKS, f"Unexpected hash: {nt_hash}"
|
|
|
|
def test_no_files_created_in_project_dir(self):
|
|
"""Verify no symlinks or output files leak into the project directory."""
|
|
self._create_hash_file()
|
|
self._create_wordlist()
|
|
|
|
nt_file = self._extract_nt_hashes()
|
|
self._run_hashcat_crack(nt_file)
|
|
self._combine_ntlm_output(f"{nt_file}.out")
|
|
|
|
# Check project root for any test_hashes artifacts
|
|
for item in REPO_ROOT.iterdir():
|
|
assert "test_hashes" not in item.name, (
|
|
f"Test artifact leaked into project dir: {item}"
|
|
)
|
|
|
|
def test_nt_extraction_output_path(self):
|
|
"""NT hash extraction writes .nt file next to the original."""
|
|
self._create_hash_file()
|
|
nt_file = self._extract_nt_hashes()
|
|
assert nt_file == f"{HASH_BASE}.nt"
|
|
assert Path(nt_file).parent == Path("/tmp")
|
|
|
|
def test_hashcat_output_path(self):
|
|
"""hashcat -o writes .nt.out next to the .nt file."""
|
|
self._create_hash_file()
|
|
self._create_wordlist()
|
|
nt_file = self._extract_nt_hashes()
|
|
nt_out = self._run_hashcat_crack(nt_file)
|
|
assert nt_out == f"{HASH_BASE}.nt.out"
|
|
assert Path(nt_out).parent == Path("/tmp")
|