threat hunting

Hunting for Command and Control Beaconing

Detect C2 beaconing patterns in network traffic using frequency analysis, jitter detection, and domain reputation to identify compromised endpoints communicating with adversary infrastructure.

beaconingc2mitre-attacknetwork-analysisproactive-detectionthreat-hunting
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

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

  1. Identify Beaconing Characteristics: Define what constitutes beaconing (regular intervals, small payload sizes, consistent destinations, jitter patterns).
  2. Collect Network Telemetry: Aggregate proxy logs, DNS queries, and connection metadata for analysis.
  3. Apply Frequency Analysis: Identify connections with regular intervals using statistical methods (standard deviation, coefficient of variation).
  4. Filter Known-Good Traffic: Exclude legitimate periodic traffic (Windows Update, AV updates, heartbeat services, NTP).
  5. Analyze Domain/IP Reputation: Check identified beaconing destinations against threat intel, WHOIS data, and certificate transparency logs.
  6. Investigate Endpoint Context: Correlate beaconing activity with process creation, user context, and file system changes on source endpoints.
  7. 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

  1. Cobalt Strike Beacon: HTTP/HTTPS beaconing with configurable sleep time and jitter to malleable C2 profiles.
  2. DNS Tunneling C2: Data exfiltration and command receipt via encoded DNS TXT/CNAME queries to attacker-controlled domains.
  3. Sliver C2 over HTTPS: Modern C2 framework using HTTPS with configurable beacon intervals and domain fronting.
  4. DGA-based C2: Malware generating random domains daily, with adversary registering upcoming domains for C2.
  5. 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: various

Threat 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_dataset
standards.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 cv

Step 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 > 30

Phase 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 -count

Step 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 > 20

Step 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 destinations

Step 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 -count

Phase 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 -count

Step 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 Computer

Phase 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

  1. Capture packet sample of suspected C2 traffic
  2. Analyze TLS certificate details
  3. Check domain/IP against multiple TI sources
  4. Review endpoint process tree
  5. Look for associated file drops or tool transfers

Step 6.2 - Response Actions

  1. Block C2 domain/IP at firewall and proxy
  2. Isolate compromised endpoint(s)
  3. Preserve forensic evidence
  4. Reset credentials used on affected systems
  5. 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 KB
Keep exploring