npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
Overview
Cobalt Strike is the most prevalent command-and-control framework used by both red teams and threat actors. Beacon, its primary payload, communicates with team servers using configurable HTTP/HTTPS/DNS profiles that can mimic legitimate traffic. However, default configurations and behavioral patterns remain detectable through TLS certificate analysis (default serial 8BB00EE), JA3/JA3S fingerprinting, beacon interval jitter analysis, and HTTP malleable profile pattern matching. This skill covers building detection capabilities using Zeek network logs, Suricata IDS rules, and Python-based PCAP analysis to identify beacon callbacks in network traffic.
When to Use
- When investigating security incidents that require hunting for cobalt strike beacons
- When building detection rules or threat hunting queries for this domain
- When SOC analysts need structured procedures for this analysis type
- When validating security monitoring coverage for related attack techniques
Prerequisites
- Zeek 6.0+ with JA3 and HASSH packages installed
- Suricata 7.0+ with Emerging Threats ruleset
- Python 3.9+ with scapy and dpkt libraries
- Network traffic captures (PCAP) or live Zeek logs
- RITA (Real Intelligence Threat Analytics) for beacon scoring
- Threat intelligence feeds with known Cobalt Strike IOCs
Steps
Step 1: TLS Certificate Analysis
Detect default Cobalt Strike certificates using JA3S fingerprints, certificate serial numbers, and JARM fingerprints in Zeek ssl.log.
Step 2: Beacon Interval Analysis
Analyze connection timing patterns to identify regular callback intervals with configurable jitter, characteristic of beacon behavior.
Step 3: HTTP Profile Detection
Match HTTP request patterns (URI paths, headers, user-agents) against known malleable C2 profiles.
Step 4: Correlate and Score
Combine multiple indicators (TLS + timing + HTTP profile) into a composite beacon confidence score.
Expected Output
JSON report containing detected beacon candidates with confidence scores, TLS fingerprints, timing analysis, HTTP profile matches, and recommended response actions.
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md4.6 KB
API Reference: Hunting for Cobalt Strike Beacons
Cobalt Strike Default TLS Indicators
| Indicator | Value | Detection Confidence |
|---|---|---|
| Default cert serial | 8BB00EE |
95% (unmodified teamserver) |
| Default cert issuer | Major Cobalt Strike |
95% |
| JA3S hash (Java TLS) | ae4edc6faf64d08308082ad26be60767 |
80% |
| JA3S hash (alt) | a0e9f5d64349fb13191bc781f81f42e1 |
80% |
| JARM fingerprint | 07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1 |
90% |
Zeek Log Fields for Detection
ssl.log Key Fields
| Field Index | Name | Use |
|---|---|---|
| 0 | ts | Connection timestamp |
| 2 | id.orig_h | Source IP |
| 4 | id.resp_h | Destination IP (C2 server) |
| 5 | id.resp_p | Destination port |
| 20 | cert_chain_fps | Certificate serial number |
| 21 | ja3s | JA3S server fingerprint hash |
conn.log Beacon Timing Fields
| Field Index | Name | Use |
|---|---|---|
| 0 | ts | Connection epoch timestamp |
| 2 | id.orig_h | Beaconing host |
| 4 | id.resp_h | C2 destination |
| 5 | id.resp_p | C2 port |
| 8 | duration | Session length |
| 9 | orig_bytes | Bytes sent (check size) |
| 10 | resp_bytes | Bytes received (check size) |
RITA Beacon Analysis
# Import Zeek logs into RITA
rita import /opt/zeek/logs/current rita_dataset
# Show beaconing connections ranked by score
rita show-beacons rita_dataset --human-readable
# Show long connections (persistent C2)
rita show-long-connections rita_dataset
# Export beacon results as CSV
rita show-beacons rita_dataset -H > beacons.csv
# Show DNS tunneling (alternate C2 channel)
rita show-exploded-dns rita_datasetSuricata Detection Rules
# Detect default Cobalt Strike TLS certificate
alert tls any any -> any any (msg:"ET MALWARE Cobalt Strike Default Certificate"; \
tls.cert_serial; content:"8BB00EE"; sid:2029560; rev:3;)
# Detect known Cobalt Strike JA3S
alert tls any any -> any any (msg:"ET MALWARE Cobalt Strike JA3S"; \
ja3s.hash; content:"ae4edc6faf64d08308082ad26be60767"; sid:2029561; rev:2;)
# Detect Cobalt Strike default HTTP beacon URI
alert http any any -> any any (msg:"ET MALWARE CobaltStrike Beacon URI"; \
content:"GET"; http_method; pcre:"/^\/[a-zA-Z]{4}$/U"; sid:2029562; rev:1;)
# Detect Cobalt Strike named pipe (SMB beacon)
alert smb any any -> any any (msg:"ET MALWARE CobaltStrike Named Pipe"; \
content:"|MSRPC|"; content:"\\\\pipe\\\\"; content:"MSSE-"; sid:2029563; rev:1;)Malleable C2 Profile HTTP Indicators
| Pattern | URI Regex | Context |
|---|---|---|
| Default GET | ^/[a-zA-Z]{4}$ |
4-char alpha URI (e.g., /aGth) |
| submit.php | ^/submit\.php\?id=\d+$ |
POST callback with numeric ID |
| Pixel tracking | ^/pixel\.(gif|png)$ |
Fake tracking pixel |
| UTM beacon | ^/__utm\.gif$ |
Mimics Google Analytics |
| RSS feed | ^/updates\.(rss|json)$ |
Fake feed endpoint |
| JS beacon | ^/visit\.js$ |
Fake JavaScript resource |
Default User-Agent Strings
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)
Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)Beacon Timing Analysis Formula
# Jitter percentage calculation
intervals = [t[i+1] - t[i] for i in range(len(t) - 1)]
avg = sum(intervals) / len(intervals)
std = sqrt(sum((x - avg)**2 for x in intervals) / len(intervals))
jitter_pct = (std / avg) * 100
# Beacon score (0-100, higher = more likely beacon)
beacon_score = max(0, 1 - (jitter_pct / 100)) * 100
# Score >= 85 = critical, >= 60 = high suspicionJARM Scanner CLI
# Scan single host for JARM fingerprint
python3 jarm.py -p 443 suspicious-host.example.com
# Known Cobalt Strike JARM
# 07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1
# Compare against threat intel JARM database
python3 jarm.py -p 8443 10.0.0.50 | grep -f cs_jarm_list.txtMITRE ATT&CK Mapping
| Technique | ID | Beacon Indicator |
|---|---|---|
| Application Layer Protocol | T1071.001 | HTTP/HTTPS beaconing pattern |
| Encrypted Channel | T1573.002 | Default TLS cert / JA3S match |
| Non-Standard Port | T1571 | HTTPS on 8080, 8443, 444 |
| Ingress Tool Transfer | T1105 | Large resp_bytes in beacon |
| Proxy | T1090 | Redirector infrastructure |
References
- JARM Scanner: https://github.com/salesforce/jarm
- RITA: https://github.com/activecm/rita
- JA3/JA3S: https://github.com/salesforce/ja3
- Cobalt Strike Detection: https://thedfirreport.com
- MITRE T1071.001: https://attack.mitre.org/techniques/T1071/001/
Scripts 1
agent.py8.1 KB
#!/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()