Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-SkillsFramework mappings
MITRE ATT&CK
T1046 on the official MITRE ATT&CK siteT1057 on the official MITRE ATT&CK siteT1071 on the official MITRE ATT&CK siteT1071.001 on the official MITRE ATT&CK siteT1071.004 on the official MITRE ATT&CK siteT1082 on the official MITRE ATT&CK siteT1083 on the official MITRE ATT&CK siteT1090 on the official MITRE ATT&CK siteT1090.002 on the official MITRE ATT&CK siteT1095 on the official MITRE ATT&CK siteT1102 on the official MITRE ATT&CK siteT1105 on the official MITRE ATT&CK siteT1132.001 on the official MITRE ATT&CK siteT1132.002 on the official MITRE ATT&CK siteT1568.001 on the official MITRE ATT&CK siteT1568.002 on the official MITRE ATT&CK siteT1572 on the official MITRE ATT&CK siteT1573.001 on the official MITRE ATT&CK siteT1573.002 on the official MITRE ATT&CK site
NIST CSF 2.0
MITRE D3FEND
Application Protocol Command Analysis on the official MITRE D3FEND siteCertificate Analysis on the official MITRE D3FEND siteContent Format Conversion on the official MITRE D3FEND siteFile Content Analysis on the official MITRE D3FEND siteFile Metadata Consistency Validation on the official MITRE D3FEND site
When to Use
- When proactively hunting for compromised systems in the network
- After threat intel indicates C2 frameworks targeting your industry
- When investigating periodic outbound connections to suspicious domains
- During incident response to identify active C2 channels
- When DNS query logs show unusual patterns to specific domains
Prerequisites
- Network proxy/firewall logs with full URL and timing data
- DNS query logs (passive DNS, DNS server logs, or Sysmon Event ID 22)
- Zeek/Bro network connection logs or NetFlow data
- SIEM with statistical analysis capabilities (Splunk, Elastic)
- Threat intelligence feeds for domain/IP reputation
Workflow
- Identify Beaconing Characteristics: Define what constitutes beaconing (regular intervals, small payload sizes, consistent destinations, jitter patterns).
- Collect Network Telemetry: Aggregate proxy logs, DNS queries, and connection metadata for analysis.
- Apply Frequency Analysis: Identify connections with regular intervals using statistical methods (standard deviation, coefficient of variation).
- Filter Known-Good Traffic: Exclude legitimate periodic traffic (Windows Update, AV updates, heartbeat services, NTP).
- Analyze Domain/IP Reputation: Check identified beaconing destinations against threat intel, WHOIS data, and certificate transparency logs.
- Investigate Endpoint Context: Correlate beaconing activity with process creation, user context, and file system changes on source endpoints.
- Confirm and Respond: Validate C2 activity, block communication, and initiate incident response.
Key Concepts
| Concept | Description |
|---|---|
| T1071 | Application Layer Protocol (HTTP/HTTPS/DNS C2) |
| T1071.001 | Web Protocols (HTTP/S beaconing) |
| T1071.004 | DNS (DNS tunneling C2) |
| T1573 | Encrypted Channel |
| T1572 | Protocol Tunneling |
| T1568 | Dynamic Resolution (DGA, fast-flux) |
| T1132 | Data Encoding in C2 |
| T1095 | Non-Application Layer Protocol |
| Beacon Interval | Time between C2 check-ins |
| Jitter | Random variation in beacon interval |
| DGA | Domain Generation Algorithm |
| Fast-Flux | Rapidly changing DNS resolution |
Tools & Systems
| Tool | Purpose |
|---|---|
| RITA (Real Intelligence Threat Analytics) | Automated beacon detection in Zeek logs |
| Splunk | Statistical beacon analysis with SPL |
| Elastic Security | ML-based anomaly detection for beaconing |
| Zeek/Bro | Network connection metadata collection |
| Suricata | Network IDS with JA3/JA4 fingerprinting |
| VirusTotal | Domain and IP reputation checking |
| PassiveDNS | Historical DNS resolution data |
| Flare | C2 profile detection |
Common Scenarios
- Cobalt Strike Beacon: HTTP/HTTPS beaconing with configurable sleep time and jitter to malleable C2 profiles.
- DNS Tunneling C2: Data exfiltration and command receipt via encoded DNS TXT/CNAME queries to attacker-controlled domains.
- Sliver C2 over HTTPS: Modern C2 framework using HTTPS with configurable beacon intervals and domain fronting.
- DGA-based C2: Malware generating random domains daily, with adversary registering upcoming domains for C2.
- Legitimate Service Abuse: C2 over legitimate cloud services (Azure, AWS, Slack, Discord, Telegram).
Output Format
Hunt ID: TH-C2-[DATE]-[SEQ]
Source IP: [Internal IP]
Source Host: [Hostname]
Destination: [Domain/IP]
Protocol: [HTTP/HTTPS/DNS/Custom]
Beacon Interval: [Average seconds]
Jitter: [Percentage]
Connection Count: [Total connections]
Data Volume: [Bytes sent/received]
First Seen: [Timestamp]
Last Seen: [Timestamp]
Domain Age: [Days]
TI Match: [Yes/No - source]
Risk Level: [Critical/High/Medium/Low]Source materials
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 3
api-reference.md2.2 KB
API Reference: C2 Beaconing Hunting
Zeek Log Files
conn.log Fields
| Index | Field | C2 Relevance |
|---|---|---|
| 0 | ts | Timing analysis |
| 2 | id.orig_h | Internal host |
| 4 | id.resp_h | C2 server |
| 5 | id.resp_p | C2 port |
| 8 | duration | Long = persistent C2 |
| 9 | orig_bytes | Upload size |
| 10 | resp_bytes | Download size |
dns.log Fields
| Index | Field | C2 Relevance |
|---|---|---|
| 0 | ts | Query timing |
| 2 | id.orig_h | Querying host |
| 9 | query | Domain queried |
| 11 | answers | Resolution |
| 14 | qtype_name | Query type (TXT = tunneling) |
http.log Fields
| Index | Field | C2 Relevance |
|---|---|---|
| 8 | host | C2 domain |
| 9 | uri | C2 path |
| 12 | user_agent | Identifies C2 framework |
| 13 | request_body_len | Upload size |
| 14 | response_body_len | Download size |
C2 Framework Signatures
| Framework | User Agent | URI Pattern | Default Port |
|---|---|---|---|
| Cobalt Strike | Mozilla/5.0 | /submit.php, /activity | 443 |
| Metasploit | (varies) | /random 4-8 chars | 4444 |
| Empire | Mozilla/5.0 | /login/process.php | 443 |
| Sliver | (custom) | /random UUID | 443 |
DNS Tunneling Indicators
| Indicator | Pattern |
|---|---|
| Long subdomain | [a-z0-9]{30,}\.domain\.com |
| High query frequency | > 100 queries/hour to one domain |
| TXT record queries | Unusual volume of TXT lookups |
| High entropy | Shannon entropy > 3.5 in subdomain |
JA3/JA3S TLS Fingerprinting
JA3 Hash (Client)
# Zeek ssl.log field: ja3
# Known C2 JA3 hashes:
# Cobalt Strike: 72a589da586844d7f0818ce684948eea
# Metasploit: variousThreat Intelligence Feeds
Abuse.ch ThreatFox
POST https://threatfox-api.abuse.ch/api/v1/
Content-Type: application/json
{"query": "search_ioc", "search_term": "1.2.3.4"}OTX AlienVault
GET https://otx.alienvault.com/api/v1/indicators/IPv4/{ip}/general
X-OTX-API-KEY: {key}RITA Beacon Analysis
rita import /path/to/zeek/logs my_dataset
rita show-beacons my_dataset
rita show-long-connections my_dataset
rita show-dns-fqdn-pairs my_datasetstandards.md3.1 KB
Standards and References - C2 Beaconing Detection
MITRE ATT&CK Command and Control (TA0011)
| Technique | Name | Indicators |
|---|---|---|
| T1071.001 | Web Protocols | HTTP/HTTPS periodic connections |
| T1071.004 | DNS | DNS query patterns, tunneling |
| T1573.001 | Symmetric Cryptography | Encrypted C2 channels |
| T1573.002 | Asymmetric Cryptography | TLS C2 with custom certs |
| T1572 | Protocol Tunneling | DNS over HTTPS, ICMP tunneling |
| T1568.002 | Domain Generation Algorithms | Random domain patterns |
| T1568.001 | Fast Flux DNS | Rapidly rotating IPs |
| T1132.001 | Standard Encoding | Base64 in C2 traffic |
| T1132.002 | Non-Standard Encoding | Custom encoding schemes |
| T1095 | Non-Application Layer Protocol | ICMP, raw TCP/UDP C2 |
| T1090 | Proxy | Multi-hop C2 infrastructure |
| T1090.002 | External Proxy | External relay points |
| T1102 | Web Service | Legitimate services for C2 |
| T1105 | Ingress Tool Transfer | Downloading tools via C2 |
Beaconing Detection Thresholds
| Metric | Threshold | Notes |
|---|---|---|
| Coefficient of Variation | < 0.20 | Strong periodicity indicator |
| Min Beacon Interval | > 30 seconds | Below may be streaming |
| Unique Destinations | Single domain/IP | C2 typically targets 1 destination |
| Session Duration | > 24 hours | Persistent C2 activity |
| Data Size Consistency | < 20% variance | Heartbeat-like payload sizes |
| Connection Count | > 50/day | Meaningful sample for analysis |
Known C2 Framework Signatures
| Framework | Default Interval | Jitter | Protocol | JA3 Hash |
|---|---|---|---|---|
| Cobalt Strike | 60s | 0-50% | HTTPS, DNS | Multiple known hashes |
| Metasploit Meterpreter | 5s | 0% | TCP, HTTP/S | Framework-dependent |
| Sliver | 60s | 0-30% | HTTPS, mTLS, WireGuard | Varies |
| Brute Ratel C4 | 60s | 10-30% | HTTPS, DNS | Varies |
| Havoc | 5s | 0-20% | HTTPS | Varies |
| Mythic | Configurable | Configurable | HTTP/S, TCP | Agent-dependent |
| Covenant | 10s | 10% | HTTP/S | .NET TLS |
| Empire/Starkiller | 5s | 0-20% | HTTP/S | Python TLS |
Data Sources
| Source | Data Type | Use |
|---|---|---|
| Zeek conn.log | Connection metadata | Duration, bytes, frequency |
| Zeek dns.log | DNS queries | Domain analysis, DGA detection |
| Zeek http.log | HTTP headers | User-agent, URI patterns |
| Zeek ssl.log | TLS metadata | JA3, certificate analysis |
| Proxy logs | Full URL, user agent | Content inspection |
| Sysmon Event 3 | Network connections | Process-to-connection mapping |
| Sysmon Event 22 | DNS queries | DNS process attribution |
| NetFlow/IPFIX | Network flows | Volume and timing analysis |
| Firewall logs | Allow/deny with timing | Connection frequency |
DNS Tunneling Indicators
| Indicator | Description |
|---|---|
| High query volume | > 100 queries/hour to single domain |
| Long subdomain labels | > 30 characters in subdomain |
| High entropy subdomains | Base32/64 encoded data |
| TXT record queries | Large TXT records for data transfer |
| NULL/CNAME responses | Unusual record types |
| Unique subdomain count | Many unique subdomains per domain |
workflows.md4.3 KB
Detailed Hunting Workflow - C2 Beaconing Detection
Phase 1: HTTP/HTTPS Beacon Detection
Step 1.1 - Splunk Frequency Analysis
index=proxy OR index=firewall
| where NOT match(dest, "(?i)(microsoft|google|amazonaws|cloudflare|akamai)")
| bin _time span=1s
| stats count by src_ip dest _time
| streamstats current=f last(_time) as prev_time by src_ip dest
| eval interval=_time-prev_time
| stats count avg(interval) as avg_interval stdev(interval) as stdev_interval min(interval) as min_interval max(interval) as max_interval by src_ip dest
| where count > 50
| eval cv=stdev_interval/avg_interval
| where cv < 0.20 AND avg_interval > 30 AND avg_interval < 86400
| sort cv
| table src_ip dest count avg_interval stdev_interval cvStep 1.2 - KQL Beacon Detection
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemoteIPType == "Public"
| summarize ConnectionTimes=make_list(Timestamp), Count=count() by DeviceName, RemoteIP, RemoteUrl
| where Count > 50
| extend Intervals = array_sort_asc(ConnectionTimes)
| mv-apply Intervals on (
extend NextTime = next(Intervals)
| where isnotempty(NextTime)
| extend IntervalSec = datetime_diff('second', NextTime, Intervals)
| summarize AvgInterval=avg(IntervalSec), StdDev=stdev(IntervalSec)
)
| extend CV = StdDev / AvgInterval
| where CV < 0.2 and AvgInterval > 30Phase 2: DNS Beaconing and Tunneling
Step 2.1 - DNS Query Frequency Analysis
index=dns
| rex field=query "(?<subdomain>[^.]+)\.(?<domain>[^.]+\.[^.]+)$"
| stats count dc(subdomain) as unique_subdomains avg(len(query)) as avg_query_len by src_ip domain
| where count > 100 AND (unique_subdomains > 50 OR avg_query_len > 40)
| sort -countStep 2.2 - DNS Entropy Analysis
index=dns query_type IN ("TXT", "NULL", "CNAME", "MX")
| rex field=query "^(?<subdomain>[^.]+)"
| eval entropy=0
| foreach * [eval entropy=entropy]
| where len(subdomain) > 20
| stats count by src_ip query domain
| where count > 20Step 2.3 - RITA-Style Beacon Analysis
RITA automatically analyzes Zeek logs for:
- Connection frequency with jitter tolerance
- DNS tunneling indicators
- Long connection durations
- Unusual user agents
Phase 3: JA3/JA4 TLS Fingerprinting
Step 3.1 - Unusual TLS Fingerprints
index=zeek sourcetype=bro_ssl
| stats count dc(id.resp_h) as unique_dests values(id.resp_h) as destinations by ja3 ja3s
| where count > 10
| lookup ja3_known_bad ja3
| where match="true"
| table ja3 ja3s count unique_dests destinationsStep 3.2 - Self-Signed Certificate Detection
index=zeek sourcetype=bro_ssl
| where validation_status!="ok"
| stats count by id.orig_h id.resp_h server_name validation_status
| where count > 10
| sort -countPhase 4: Process-Level Correlation
Step 4.1 - Map Processes to Network Connections
index=sysmon EventCode=3
| where NOT match(DestinationIp, "^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)")
| stats count values(DestinationPort) as ports dc(DestinationIp) as unique_ips by Image Computer
| where count > 50 AND unique_ips < 5
| sort -countStep 4.2 - Unusual Process Network Activity
index=sysmon EventCode=3
| where match(Image, "(?i)(notepad|calc|mspaint|write|wordpad)")
| stats count by Image DestinationIp DestinationPort ComputerPhase 5: Domain Intelligence
Step 5.1 - New/Young Domain Detection
Check domains seen in beaconing analysis:
- WHOIS creation date < 30 days
- Domain registered with privacy protection
- Hosting on bulletproof infrastructure
- No historical passive DNS data
Step 5.2 - DGA Domain Detection
Indicators of algorithmically generated domains:
- High character entropy (> 3.5 bits per char)
- No dictionary words in domain
- Unusual TLD combinations
- Sequential registration patterns
Phase 6: Verification and Response
Step 6.1 - Confirm C2 Activity
- Capture packet sample of suspected C2 traffic
- Analyze TLS certificate details
- Check domain/IP against multiple TI sources
- Review endpoint process tree
- Look for associated file drops or tool transfers
Step 6.2 - Response Actions
- Block C2 domain/IP at firewall and proxy
- Isolate compromised endpoint(s)
- Preserve forensic evidence
- Reset credentials used on affected systems
- Hunt for additional infected hosts using same IOCs
Scripts 2
agent.py6.1 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for hunting C2 beaconing across multiple data sources."""
import argparse
import json
import re
from collections import defaultdict
from datetime import datetime, timezone
C2_INDICATORS = {
"known_ports": {443, 8443, 8080, 4444, 5555, 8888, 9090, 1337},
"suspicious_user_agents": [
"mozilla/4.0", "python-requests", "curl/", "wget/",
"java/", "go-http-client",
],
"dns_c2_patterns": [
r'^[a-z0-9]{30,}\.', # Long random subdomain (DNS tunneling)
r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', # Direct IP
],
}
def analyze_dns_queries(dns_log_path):
"""Analyze DNS query logs for C2 indicators."""
findings = []
domain_counts = defaultdict(int)
try:
with open(dns_log_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
fields = line.strip().split("\t")
if len(fields) < 10:
continue
query = fields[9] if len(fields) > 9 else ""
domain_counts[query] += 1
for pattern in C2_INDICATORS["dns_c2_patterns"]:
if re.match(pattern, query):
findings.append({
"type": "suspicious_dns",
"query": query,
"pattern": pattern,
})
except FileNotFoundError:
pass
high_freq = sorted(domain_counts.items(), key=lambda x: x[1], reverse=True)[:20]
for domain, count in high_freq:
if count > 100 and len(domain) > 20:
findings.append({
"type": "high_frequency_dns",
"domain": domain,
"query_count": count,
})
return findings
def analyze_http_logs(http_log_path):
"""Analyze HTTP logs for C2-like traffic patterns."""
findings = []
try:
with open(http_log_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
fields = line.strip().split("\t")
if len(fields) < 13:
continue
host = fields[8] if len(fields) > 8 else ""
uri = fields[9] if len(fields) > 9 else ""
user_agent = fields[12] if len(fields) > 12 else ""
for ua in C2_INDICATORS["suspicious_user_agents"]:
if ua in user_agent.lower():
findings.append({
"type": "suspicious_user_agent",
"host": host,
"uri": uri[:100],
"user_agent": user_agent[:100],
})
break
if re.match(r'^/[a-zA-Z0-9]{4,8}$', uri):
findings.append({
"type": "c2_uri_pattern",
"host": host,
"uri": uri,
"note": "Short random URI typical of C2 frameworks",
})
except FileNotFoundError:
pass
return findings
def analyze_connection_patterns(conn_log_path):
"""Detect persistent long-duration connections typical of C2."""
findings = []
try:
with open(conn_log_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
fields = line.strip().split("\t")
if len(fields) < 10:
continue
src = fields[2]
dst = fields[4]
dst_port = fields[5]
duration = fields[8] if len(fields) > 8 else "0"
orig_bytes = fields[9] if len(fields) > 9 else "0"
resp_bytes = fields[10] if len(fields) > 10 else "0"
try:
dur = float(duration) if duration != "-" else 0
ob = int(orig_bytes) if orig_bytes != "-" else 0
rb = int(resp_bytes) if resp_bytes != "-" else 0
except ValueError:
continue
if dur > 3600 and ob > 0 and rb > 0:
ratio = ob / rb if rb > 0 else 999
if 0.8 < ratio < 1.2:
findings.append({
"type": "persistent_symmetric",
"src": src, "dst": dst, "port": dst_port,
"duration_hours": round(dur / 3600, 1),
"data_ratio": round(ratio, 2),
})
except FileNotFoundError:
pass
return findings
def main():
parser = argparse.ArgumentParser(
description="Hunt for C2 beaconing across network data sources"
)
parser.add_argument("--conn-log", help="Zeek conn.log")
parser.add_argument("--dns-log", help="Zeek dns.log")
parser.add_argument("--http-log", help="Zeek http.log")
parser.add_argument("--output", "-o", help="Output JSON report")
args = parser.parse_args()
print("[*] C2 Beaconing Hunting Agent")
report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": {}}
if args.dns_log:
dns = analyze_dns_queries(args.dns_log)
report["findings"]["dns"] = dns
print(f"[*] DNS findings: {len(dns)}")
if args.http_log:
http = analyze_http_logs(args.http_log)
report["findings"]["http"] = http
print(f"[*] HTTP findings: {len(http)}")
if args.conn_log:
conn = analyze_connection_patterns(args.conn_log)
report["findings"]["connections"] = conn
print(f"[*] Connection findings: {len(conn)}")
total = sum(len(v) for v in report["findings"].values())
report["risk_level"] = "CRITICAL" if total >= 10 else "HIGH" if total >= 5 else "MEDIUM" if total > 0 else "LOW"
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"[*] Report saved to {args.output}")
else:
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()
process.py14.5 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
C2 Beaconing Detection Script
Analyzes network connection logs for periodic beaconing patterns
using statistical frequency analysis and jitter detection.
"""
import json
import csv
import argparse
import datetime
import math
import re
from collections import defaultdict
from pathlib import Path
# Known legitimate beaconing services to exclude
KNOWN_GOOD_DOMAINS = {
"microsoft.com", "windowsupdate.com", "google.com", "googleapis.com",
"gstatic.com", "amazonaws.com", "cloudflare.com", "akamai.net",
"apple.com", "icloud.com", "adobe.com", "symantec.com",
"norton.com", "mcafee.com", "crowdstrike.com", "sentinelone.com",
"office365.com", "office.com", "live.com", "outlook.com",
"github.com", "slack.com", "teams.microsoft.com",
}
# Known C2 framework default ports
C2_SUSPICIOUS_PORTS = {443, 8443, 8080, 4444, 5555, 6666, 8888, 9090, 50050, 31337}
# Beaconing detection thresholds
BEACON_THRESHOLDS = {
"min_connections": 20, # Minimum connections for analysis
"max_cv": 0.25, # Max coefficient of variation for periodicity
"min_interval": 10, # Minimum average interval (seconds)
"max_interval": 86400, # Maximum average interval (1 day)
"max_data_cv": 0.30, # Max CV for data size consistency
}
def parse_logs(input_path: str) -> list[dict]:
"""Parse connection logs (Zeek, CSV, JSON format)."""
path = Path(input_path)
events = []
if path.suffix == ".json":
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
events = data if isinstance(data, list) else data.get("events", [])
elif path.suffix == ".csv":
with open(path, "r", encoding="utf-8-sig") as f:
events = [dict(row) for row in csv.DictReader(f)]
elif path.suffix == ".log":
# Zeek tab-separated format
with open(path, "r", encoding="utf-8") as f:
headers = None
for line in f:
if line.startswith("#fields"):
headers = line.strip().split("\t")[1:]
elif line.startswith("#"):
continue
elif headers:
values = line.strip().split("\t")
if len(values) == len(headers):
events.append(dict(zip(headers, values)))
return events
def normalize_connection(event: dict) -> dict:
"""Normalize connection event fields."""
field_map = {
"timestamp": ["ts", "timestamp", "_time", "@timestamp", "Timestamp"],
"src_ip": ["id.orig_h", "src_ip", "source_ip", "LocalIP", "DeviceName"],
"src_port": ["id.orig_p", "src_port", "source_port", "LocalPort"],
"dst_ip": ["id.resp_h", "dst_ip", "dest_ip", "RemoteIP", "DestinationIp"],
"dst_port": ["id.resp_p", "dst_port", "dest_port", "RemotePort", "DestinationPort"],
"domain": ["query", "domain", "host", "RemoteUrl", "server_name", "dest"],
"bytes_sent": ["orig_bytes", "bytes_out", "SentBytes", "bytes_sent"],
"bytes_recv": ["resp_bytes", "bytes_in", "ReceivedBytes", "bytes_recv"],
"duration": ["duration", "conn_duration", "session_duration"],
"proto": ["proto", "protocol", "Protocol"],
"user_agent": ["user_agent", "UserAgent", "http_user_agent"],
}
normalized = {}
for target, sources in field_map.items():
for src in sources:
if src in event and event[src] and event[src] != "-":
normalized[target] = str(event[src])
break
if target not in normalized:
normalized[target] = ""
return normalized
def is_known_good(domain: str) -> bool:
"""Check if domain is in known-good list."""
domain_lower = domain.lower()
for good in KNOWN_GOOD_DOMAINS:
if domain_lower.endswith(good):
return True
return False
def calculate_entropy(text: str) -> float:
"""Calculate Shannon entropy of a string."""
if not text:
return 0.0
freq = defaultdict(int)
for char in text:
freq[char] += 1
length = len(text)
entropy = 0.0
for count in freq.values():
p = count / length
if p > 0:
entropy -= p * math.log2(p)
return entropy
def detect_beaconing(connections: list[dict]) -> list[dict]:
"""Analyze connection patterns for beaconing behavior."""
# Group connections by source-destination pair
pairs = defaultdict(list)
for conn in connections:
src = conn.get("src_ip", "")
dst = conn.get("domain", "") or conn.get("dst_ip", "")
if src and dst and not is_known_good(dst):
try:
ts = float(conn.get("timestamp", 0))
except (ValueError, TypeError):
# Try parsing ISO timestamp
try:
dt = datetime.datetime.fromisoformat(conn["timestamp"].replace("Z", "+00:00"))
ts = dt.timestamp()
except (ValueError, KeyError):
continue
pairs[(src, dst)].append({
"timestamp": ts,
"bytes_sent": int(conn.get("bytes_sent", 0) or 0),
"bytes_recv": int(conn.get("bytes_recv", 0) or 0),
"dst_port": conn.get("dst_port", ""),
"user_agent": conn.get("user_agent", ""),
})
findings = []
for (src, dst), conns in pairs.items():
if len(conns) < BEACON_THRESHOLDS["min_connections"]:
continue
# Sort by timestamp
conns.sort(key=lambda x: x["timestamp"])
# Calculate intervals
intervals = []
for i in range(1, len(conns)):
interval = conns[i]["timestamp"] - conns[i - 1]["timestamp"]
if interval > 0:
intervals.append(interval)
if len(intervals) < 10:
continue
# Statistical analysis
avg_interval = sum(intervals) / len(intervals)
if avg_interval < BEACON_THRESHOLDS["min_interval"] or avg_interval > BEACON_THRESHOLDS["max_interval"]:
continue
variance = sum((x - avg_interval) ** 2 for x in intervals) / len(intervals)
stdev = math.sqrt(variance)
cv = stdev / avg_interval if avg_interval > 0 else float("inf")
# Check if beaconing threshold met
if cv > BEACON_THRESHOLDS["max_cv"]:
continue
# Calculate data size consistency
bytes_sent_list = [c["bytes_sent"] for c in conns if c["bytes_sent"] > 0]
data_cv = 0.0
if bytes_sent_list:
avg_bytes = sum(bytes_sent_list) / len(bytes_sent_list)
if avg_bytes > 0:
data_var = sum((x - avg_bytes) ** 2 for x in bytes_sent_list) / len(bytes_sent_list)
data_cv = math.sqrt(data_var) / avg_bytes
# Calculate risk score
risk = 0
indicators = []
# Low CV = high periodicity
if cv < 0.05:
risk += 40
indicators.append(f"Very regular interval (CV={cv:.4f})")
elif cv < 0.15:
risk += 30
indicators.append(f"Regular interval (CV={cv:.4f})")
else:
risk += 20
indicators.append(f"Moderately regular interval (CV={cv:.4f})")
# Consistent data sizes
if data_cv < 0.10 and bytes_sent_list:
risk += 15
indicators.append(f"Very consistent payload size (CV={data_cv:.4f})")
# Suspicious port
dst_ports = set(c["dst_port"] for c in conns)
for port in dst_ports:
try:
if int(port) in C2_SUSPICIOUS_PORTS:
risk += 10
indicators.append(f"Suspicious port: {port}")
except ValueError:
pass
# High connection count
if len(conns) > 500:
risk += 10
indicators.append(f"High connection count: {len(conns)}")
# Domain entropy (DGA indicator)
domain_parts = dst.split(".")
if domain_parts:
entropy = calculate_entropy(domain_parts[0])
if entropy > 3.5:
risk += 15
indicators.append(f"High domain entropy: {entropy:.2f} (possible DGA)")
risk_level = (
"CRITICAL" if risk >= 70 else "HIGH" if risk >= 50
else "MEDIUM" if risk >= 30 else "LOW"
)
# Estimate jitter percentage
jitter_pct = (stdev / avg_interval * 100) if avg_interval > 0 else 0
findings.append({
"src_ip": src,
"destination": dst,
"connection_count": len(conns),
"avg_interval_sec": round(avg_interval, 2),
"stdev_interval": round(stdev, 2),
"coefficient_of_variation": round(cv, 4),
"estimated_jitter_pct": round(jitter_pct, 1),
"avg_bytes_sent": round(sum(bytes_sent_list) / len(bytes_sent_list)) if bytes_sent_list else 0,
"data_size_cv": round(data_cv, 4),
"first_seen": datetime.datetime.fromtimestamp(conns[0]["timestamp"]).isoformat(),
"last_seen": datetime.datetime.fromtimestamp(conns[-1]["timestamp"]).isoformat(),
"dst_ports": list(dst_ports),
"risk_score": risk,
"risk_level": risk_level,
"indicators": indicators,
})
return sorted(findings, key=lambda x: x["risk_score"], reverse=True)
def detect_dns_tunneling(connections: list[dict]) -> list[dict]:
"""Detect DNS tunneling indicators."""
domain_stats = defaultdict(lambda: {"queries": 0, "unique_subdomains": set(), "total_length": 0, "txt_queries": 0})
for conn in connections:
domain = conn.get("domain", "")
if not domain:
continue
parts = domain.split(".")
if len(parts) < 3:
continue
base_domain = ".".join(parts[-2:])
subdomain = ".".join(parts[:-2])
stats = domain_stats[base_domain]
stats["queries"] += 1
stats["unique_subdomains"].add(subdomain)
stats["total_length"] += len(domain)
findings = []
for base_domain, stats in domain_stats.items():
if stats["queries"] < 50:
continue
avg_len = stats["total_length"] / stats["queries"]
unique_subs = len(stats["unique_subdomains"])
risk = 0
indicators = []
if unique_subs > 100:
risk += 30
indicators.append(f"High unique subdomain count: {unique_subs}")
if avg_len > 40:
risk += 25
indicators.append(f"Long average query length: {avg_len:.1f}")
if stats["queries"] > 500:
risk += 15
indicators.append(f"High query volume: {stats['queries']}")
# Check subdomain entropy
for sub in list(stats["unique_subdomains"])[:10]:
ent = calculate_entropy(sub)
if ent > 3.5:
risk += 20
indicators.append(f"High subdomain entropy: {ent:.2f}")
break
if risk >= 30:
risk_level = "CRITICAL" if risk >= 70 else "HIGH" if risk >= 50 else "MEDIUM"
findings.append({
"detection_type": "DNS_TUNNELING",
"domain": base_domain,
"query_count": stats["queries"],
"unique_subdomains": unique_subs,
"avg_query_length": round(avg_len, 1),
"risk_score": risk,
"risk_level": risk_level,
"indicators": indicators,
})
return sorted(findings, key=lambda x: x["risk_score"], reverse=True)
def run_hunt(input_path: str, output_dir: str) -> None:
"""Execute C2 beaconing hunt."""
print(f"[*] C2 Beaconing Hunt - {datetime.datetime.now().isoformat()}")
connections = parse_logs(input_path)
normalized = [normalize_connection(c) for c in connections]
print(f"[*] Loaded {len(normalized)} connections")
beacon_findings = detect_beaconing(normalized)
dns_findings = detect_dns_tunneling(normalized)
all_findings = beacon_findings + dns_findings
print(f"[*] Beacon detections: {len(beacon_findings)}")
print(f"[*] DNS tunnel detections: {len(dns_findings)}")
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
with open(output_path / "c2_beacon_findings.json", "w", encoding="utf-8") as f:
json.dump({
"hunt_id": f"TH-C2-{datetime.date.today().isoformat()}",
"total_connections": len(normalized),
"beacon_findings": len(beacon_findings),
"dns_tunnel_findings": len(dns_findings),
"findings": all_findings,
}, f, indent=2)
with open(output_path / "hunt_report.md", "w", encoding="utf-8") as f:
f.write(f"# C2 Beaconing Hunt Report\n\n")
f.write(f"**Date**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"**Connections Analyzed**: {len(normalized)}\n\n")
f.write("## Beaconing Detections\n\n")
for bf in beacon_findings[:20]:
f.write(f"### [{bf['risk_level']}] {bf['src_ip']} -> {bf['destination']}\n")
f.write(f"- Interval: {bf['avg_interval_sec']}s (CV: {bf['coefficient_of_variation']})\n")
f.write(f"- Jitter: ~{bf['estimated_jitter_pct']}%\n")
f.write(f"- Connections: {bf['connection_count']}\n\n")
f.write("## DNS Tunneling Detections\n\n")
for df in dns_findings[:10]:
f.write(f"### [{df['risk_level']}] {df['domain']}\n")
f.write(f"- Queries: {df['query_count']}, Unique Subdomains: {df['unique_subdomains']}\n\n")
print(f"[+] Results written to {output_dir}")
def main():
parser = argparse.ArgumentParser(description="C2 Beaconing Detection")
subparsers = parser.add_subparsers(dest="command")
hunt_p = subparsers.add_parser("hunt")
hunt_p.add_argument("--input", "-i", required=True)
hunt_p.add_argument("--output", "-o", default="./c2_hunt_output")
subparsers.add_parser("queries", help="Print hunting queries")
args = parser.parse_args()
if args.command == "hunt":
run_hunt(args.input, args.output)
elif args.command == "queries":
print("=== Splunk Beaconing Queries ===\n")
print("--- HTTP/S Beacon Frequency ---")
print("""index=proxy
| bin _time span=1s
| stats count by src_ip dest _time
| streamstats current=f last(_time) as prev_time by src_ip dest
| eval interval=_time-prev_time
| stats count avg(interval) as avg stdev(interval) as sd by src_ip dest
| eval cv=sd/avg
| where count>50 AND cv<0.20 AND avg>30
| sort cv""")
else:
parser.print_help()
if __name__ == "__main__":
main()
Assets 1
template.mdtext/markdown · 1.0 KBKeep exploring