Files
IoT_PreTester_V46_Demo/django_project/analyzer/enrich/hash_crack.py
T
2026-08-19 08:04:59 +02:00

145 lines
4.9 KiB
Python

"""Run bounded offline hashcat attacks against supported capture artifacts."""
from __future__ import annotations
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import time
from dataclasses import dataclass, field
from typing import Callable, Optional, Sequence, Union
from ..artifacts import HashArtifact, identify_hash_mode
@dataclass
class HashCrack:
value: str
hashcat_mode: Optional[int] = None
hash_format: Optional[str] = None
cracked: bool = False
plaintext: Optional[str] = None
duration_seconds: Optional[float] = None
# pending | cracked | not_cracked | timeout | error | unsupported
state: str = "pending"
error: Optional[str] = None
def to_dict(self) -> dict:
return {
"value": self.value, "hashcat_mode": self.hashcat_mode,
"hash_format": self.hash_format, "cracked": self.cracked,
"plaintext": self.plaintext, "duration_seconds": self.duration_seconds,
"state": self.state, "error": self.error,
}
# Keep the bundled demo list first; authorized deployments may add a system list.
DEFAULT_WORDLISTS = (
str(Path(__file__).resolve().parents[3] / "demo_files" / "demo_wordlist.txt"),
"/usr/share/wordlists/rockyou.txt",
)
def build_crack_command(hashcat_path: str, mode: int, hash_file: str,
wordlists: Sequence[str], potfile: str) -> list[str]:
"""Build the hashcat dictionary-attack command."""
cmd = [hashcat_path, "-m", str(mode), "-a", "0", "-D", "1,2",
hash_file, *wordlists, "--potfile-path", potfile, "--force"]
return cmd
def build_show_command(hashcat_path: str, mode: int, hash_file: str,
potfile: str) -> list[str]:
"""Build the hashcat command used to read cracked results."""
return [hashcat_path, "-m", str(mode), "--show", "--potfile-path", potfile, hash_file]
def parse_show_output(stdout: str) -> Optional[str]:
"""Extract plaintext from ``hashcat --show`` output."""
if not stdout:
return None
for line in stdout.strip().splitlines():
if ":" in line:
return line.rsplit(":", 1)[1]
return None
def crack_hash(
artifact: Union[HashArtifact, str],
*,
wordlists: Sequence[str] = DEFAULT_WORDLISTS,
hashcat_path: str = "hashcat",
timeout: int = 300,
potfile: Optional[str] = None,
runner: Callable[..., subprocess.CompletedProcess] = subprocess.run,
which: Callable[[str], Optional[str]] = shutil.which,
) -> HashCrack:
"""Crack one artifact and convert runtime failures into a result state."""
if isinstance(artifact, HashArtifact):
value = artifact.value
mode = artifact.hashcat_mode
fmt = artifact.hash_format
else:
value = str(artifact)
mode = None
fmt = None
if mode is None:
mode, fmt = identify_hash_mode(value, hint_format=fmt)
result = HashCrack(value=value, hashcat_mode=mode, hash_format=fmt)
if mode is None:
result.state = "unsupported"
result.error = "No supported hashcat mode was detected"
return result
if which(hashcat_path) is None:
result.state = "error"
result.error = "hashcat is not installed or not available on PATH"
return result
available = [w for w in wordlists if os.path.exists(w)]
if not available:
result.state = "error"
result.error = "No wordlist is available"
return result
tmpdir = tempfile.mkdtemp(prefix="hashcrack_")
hash_file = os.path.join(tmpdir, "hash.txt")
pot = potfile or os.path.join(tmpdir, "hashcat.potfile")
try:
with open(hash_file, "w", encoding="utf-8") as fh:
fh.write(value.strip() + "\n")
start = time.time()
try:
runner(build_crack_command(hashcat_path, mode, hash_file, available, pot),
capture_output=True, text=True, timeout=timeout)
except subprocess.TimeoutExpired:
result.state = "timeout"
result.duration_seconds = round(time.time() - start, 2)
result.error = f"hashcat timed out after {timeout}s"
return result
show = runner(build_show_command(hashcat_path, mode, hash_file, pot),
capture_output=True, text=True, timeout=timeout)
result.duration_seconds = round(time.time() - start, 2)
plaintext = parse_show_output(getattr(show, "stdout", "") or "")
if plaintext is not None:
result.cracked = True
result.plaintext = plaintext
result.state = "cracked"
else:
result.state = "not_cracked"
return result
except Exception as exc: # Enrichment failures must not stop capture analysis.
result.state = "error"
result.error = str(exc)[:300]
return result
finally:
shutil.rmtree(tmpdir, ignore_errors=True)