build 4.6 Demo
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user