mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-15 20:25:18 +03:00
191 lines
8.1 KiB
Python
191 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Cobalt Strike Beacon Hunter - detects beacon signatures in network traffic and Zeek logs."""
|
|
|
|
import json
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import re
|
|
import math
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CS_DEFAULT_CERT_SERIAL = "8bb00ee"
|
|
CS_KNOWN_JA3S = [
|
|
"ae4edc6faf64d08308082ad26be60767",
|
|
"a0e9f5d64349fb13191bc781f81f42e1",
|
|
]
|
|
CS_KNOWN_JARM = "07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1"
|
|
|
|
|
|
def parse_zeek_ssl_log(ssl_log_path):
|
|
"""Parse Zeek ssl.log for default Cobalt Strike certificates."""
|
|
findings = []
|
|
try:
|
|
with open(ssl_log_path) as f:
|
|
for line in f:
|
|
if line.startswith("#"):
|
|
continue
|
|
fields = line.strip().split("\t")
|
|
if len(fields) < 22:
|
|
continue
|
|
ts, uid, src_ip, src_port, dst_ip, dst_port = fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]
|
|
serial = fields[20] if len(fields) > 20 else ""
|
|
ja3s = fields[21] if len(fields) > 21 else ""
|
|
if serial.lower().replace(":", "") == CS_DEFAULT_CERT_SERIAL:
|
|
findings.append({
|
|
"indicator": "default_cs_certificate",
|
|
"src_ip": src_ip, "dst_ip": dst_ip, "dst_port": dst_port,
|
|
"cert_serial": serial, "timestamp": ts,
|
|
"severity": "critical", "confidence": 95,
|
|
})
|
|
if ja3s in CS_KNOWN_JA3S:
|
|
findings.append({
|
|
"indicator": "known_cs_ja3s",
|
|
"src_ip": src_ip, "dst_ip": dst_ip, "dst_port": dst_port,
|
|
"ja3s_hash": ja3s, "timestamp": ts,
|
|
"severity": "high", "confidence": 80,
|
|
})
|
|
except FileNotFoundError:
|
|
logger.warning("Zeek ssl.log not found: %s", ssl_log_path)
|
|
return findings
|
|
|
|
|
|
def analyze_beacon_timing(conn_log_path, min_connections=20, max_jitter_pct=25):
|
|
"""Analyze connection timing for beacon-like regular intervals."""
|
|
connections = defaultdict(list)
|
|
try:
|
|
with open(conn_log_path) as f:
|
|
for line in f:
|
|
if line.startswith("#"):
|
|
continue
|
|
fields = line.strip().split("\t")
|
|
if len(fields) < 7:
|
|
continue
|
|
ts = float(fields[0])
|
|
src_ip, dst_ip, dst_port = fields[2], fields[4], fields[5]
|
|
key = (src_ip, dst_ip, dst_port)
|
|
connections[key].append(ts)
|
|
except (FileNotFoundError, ValueError):
|
|
logger.warning("Zeek conn.log parse failed: %s", conn_log_path)
|
|
return []
|
|
beacons = []
|
|
for (src, dst, port), timestamps in connections.items():
|
|
if len(timestamps) < min_connections:
|
|
continue
|
|
timestamps.sort()
|
|
intervals = [timestamps[i + 1] - timestamps[i] for i in range(len(timestamps) - 1)]
|
|
if not intervals:
|
|
continue
|
|
avg_interval = sum(intervals) / len(intervals)
|
|
if avg_interval < 1:
|
|
continue
|
|
std_interval = math.sqrt(sum((x - avg_interval) ** 2 for x in intervals) / len(intervals))
|
|
jitter_pct = (std_interval / avg_interval) * 100 if avg_interval > 0 else 100
|
|
if jitter_pct <= max_jitter_pct:
|
|
beacon_score = round(max(0, 1 - (jitter_pct / 100)) * 100, 1)
|
|
if beacon_score >= 60:
|
|
beacons.append({
|
|
"indicator": "beacon_timing",
|
|
"src_ip": src, "dst_ip": dst, "dst_port": port,
|
|
"connections": len(timestamps),
|
|
"avg_interval_sec": round(avg_interval, 1),
|
|
"jitter_pct": round(jitter_pct, 1),
|
|
"beacon_score": beacon_score,
|
|
"severity": "critical" if beacon_score >= 85 else "high",
|
|
"confidence": int(beacon_score),
|
|
})
|
|
return sorted(beacons, key=lambda x: x["beacon_score"], reverse=True)
|
|
|
|
|
|
def check_http_profiles(http_log_path):
|
|
"""Detect known Cobalt Strike HTTP malleable C2 profile patterns."""
|
|
cs_uri_patterns = [
|
|
r"^/[a-zA-Z]{4}$", r"^/submit\.php\?id=\d+$", r"^/pixel\.(gif|png)$",
|
|
r"^/__utm\.gif$", r"^/updates\.(rss|json)$", r"^/visit\.js$",
|
|
]
|
|
cs_ua_patterns = [
|
|
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
|
|
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
|
|
]
|
|
findings = []
|
|
try:
|
|
with open(http_log_path) as f:
|
|
for line in f:
|
|
if line.startswith("#"):
|
|
continue
|
|
fields = line.strip().split("\t")
|
|
if len(fields) < 12:
|
|
continue
|
|
src_ip, dst_ip = fields[2], fields[4]
|
|
uri = fields[9] if len(fields) > 9 else ""
|
|
user_agent = fields[12] if len(fields) > 12 else ""
|
|
for pattern in cs_uri_patterns:
|
|
if re.match(pattern, uri):
|
|
findings.append({
|
|
"indicator": "cs_http_profile",
|
|
"src_ip": src_ip, "dst_ip": dst_ip,
|
|
"uri": uri, "user_agent": user_agent[:100],
|
|
"matched_pattern": pattern,
|
|
"severity": "high", "confidence": 60,
|
|
})
|
|
break
|
|
if user_agent in cs_ua_patterns:
|
|
findings.append({
|
|
"indicator": "cs_default_user_agent",
|
|
"src_ip": src_ip, "dst_ip": dst_ip,
|
|
"user_agent": user_agent,
|
|
"severity": "high", "confidence": 70,
|
|
})
|
|
except FileNotFoundError:
|
|
logger.warning("Zeek http.log not found: %s", http_log_path)
|
|
return findings
|
|
|
|
|
|
def generate_report(tls_findings, beacon_findings, http_findings):
|
|
all_findings = tls_findings + beacon_findings + http_findings
|
|
critical = sum(1 for f in all_findings if f.get("severity") == "critical")
|
|
by_dst = defaultdict(int)
|
|
for f in all_findings:
|
|
by_dst[f.get("dst_ip", "")] += 1
|
|
return {
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"tls_certificate_hits": len(tls_findings),
|
|
"beacon_timing_detections": len(beacon_findings),
|
|
"http_profile_matches": len(http_findings),
|
|
"total_indicators": len(all_findings),
|
|
"critical_indicators": critical,
|
|
"top_suspect_destinations": dict(sorted(by_dst.items(), key=lambda x: x[1], reverse=True)[:10]),
|
|
"findings": all_findings[:30],
|
|
"cobalt_strike_likely": critical > 0,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Cobalt Strike Beacon Hunting Agent")
|
|
parser.add_argument("--zeek-dir", required=True, help="Directory containing Zeek log files")
|
|
parser.add_argument("--min-connections", type=int, default=20, help="Minimum connections for beacon analysis")
|
|
parser.add_argument("--max-jitter", type=int, default=25, help="Maximum jitter percentage for beacon scoring")
|
|
parser.add_argument("--output", default="cobalt_strike_hunt_report.json")
|
|
args = parser.parse_args()
|
|
|
|
ssl_log = os.path.join(args.zeek_dir, "ssl.log")
|
|
conn_log = os.path.join(args.zeek_dir, "conn.log")
|
|
http_log = os.path.join(args.zeek_dir, "http.log")
|
|
tls_findings = parse_zeek_ssl_log(ssl_log)
|
|
beacon_findings = analyze_beacon_timing(conn_log, args.min_connections, args.max_jitter)
|
|
http_findings = check_http_profiles(http_log)
|
|
report = generate_report(tls_findings, beacon_findings, http_findings)
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
logger.info("CS Hunt: %d TLS hits, %d beacons, %d HTTP matches, CS likely: %s",
|
|
len(tls_findings), len(beacon_findings), len(http_findings), report["cobalt_strike_likely"])
|
|
print(json.dumps(report, indent=2, default=str))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|