mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-08-28 04:09:40 +03:00
feat: add 4 new cybersecurity skills - UEBA insider threat, BeyondCorp zero trust, Linux kernel rootkits, CobaltStrike beacon hunting
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user