« Phase 08

WARMUP — C2 Infrastructure

Table of Contents


Chapter 1: C2 Architecture End-to-End

The Stack

A fully-built C2 infrastructure has four distinct components arranged in a relay chain. Here is the canonical layout:

Operator Console  ──►  Team Server  ──►  Redirector (CDN / VPS)  ──►  Implant / Beacon
      │                    │                      │                          │
  Cobalt Strike        Teamserver.c2          Apache/Nginx               svchost.exe
  operator GUI          port 50050           mod_rewrite               (injected DLL)

Each layer has a distinct role, a distinct trust relationship with its neighbors, and distinct telemetry exposure. The entire point of this architecture is to make each layer expendable without burning the others.

Operator Console

The operator console is the GUI or CLI that the red team operator uses to issue commands, view check-ins, manage listeners, and receive output. In Cobalt Strike the operator connects to the team server over TCP port 50050 using a shared password and a per-engagement cryptographic keypair. The operator sees a "beacon" object for each active implant — a row representing a connected host, its last check-in time, its privilege level, and the listener it uses.

The operator never communicates directly with the implant. Every command the operator issues (run whoami, dump LSASS, lateral-move to host X) is stored server-side and delivered to the implant on its next check-in. This decoupling is fundamental: the implant is pull-based, not push-based. If the team server disappears, the implant keeps sleeping and waiting.

The operator console also controls the listener configuration — the parameters the implant uses to check in. This includes the callback host (which is the redirector's domain, not the team server), the callback port, the HTTP method, the URI path, and the sleep/jitter values.

Team Server

The team server is the actual C2 server. It has two faces:

  1. It listens on a high port (50050 for Cobalt Strike) for authenticated operator connections. This port should never be exposed to the public internet — it is reached over SSH tunnel or a VPN from the operator's workstation.
  2. It listens on 80 and/or 443 for beacon callbacks. However — and this is the critical point — in a well-configured engagement, the team server's IP is never in the beacon's config. The beacon only knows the redirector. The team server's callback listener accepts connections forwarded by the redirector.

The team server maintains per-beacon state: which tasks are pending, which tasks have been delivered, the beacon's metadata (hostname, username, process ID, architecture, sleep interval), and the output history. This state is critical — if the team server is lost the operator loses all context about active beacons.

The team server is the component that an IR team most wants to find. Burning the redirector is inconvenient (the operator spins up a new one); burning the team server is engagement-ending. This is why the two-hop model is non-negotiable for professional engagements.

Redirector

The redirector is a lightweight relay. It is a VPS or CDN endpoint that accepts HTTP/S connections from beacons and forwards them to the team server. It adds three critical capabilities:

IP protection. The beacon's config contains only the redirector's IP or domain. If the target's IR team finds the beacon and pulls its config, they see only the redirector. The team server IP is not in the config.

Traffic categorization. The redirector domain is pre-registered days or weeks before the engagement, with a plausible cover story (technology consulting firm, CDN edge node). It has a valid TLS certificate from a public CA. When the proxy team at Meridian Freight looks up the category of the destination domain, it shows "Technology" or "Content Delivery," not "Uncategorized." Uncategorized new domains are immediately suspicious.

Traffic blending. The redirector can serve real content to anything that doesn't look like a beacon check-in. An IR analyst manually browsing to the redirector's domain sees a plausible website, not a blank page or a refused connection. A Shodan scanner sees a normal web server. Only connections that match the expected User-Agent, URI path, and header pattern are forwarded to the team server; everything else is redirected to a legitimate site with a 302.

Implant / Beacon

The implant is the code running inside a host process on the target (e.g., a DLL injected into svchost.exe, a reflectively-loaded PE in explorer.exe). It operates in a loop: sleep for the configured interval (with jitter), wake up, build an HTTP/S request to the redirector, send it, parse the response for pending tasks, execute any tasks, POST the results back, sleep again.

The implant has no persistent network connections. Each check-in is a fresh HTTP request. From a NetFlow perspective, it looks like a process periodically making outbound HTTP/S connections to a CDN endpoint.

Two-Hop Trust Model

The trust model across the stack:

  • Operator → Team Server: Mutually authenticated. Cobalt Strike uses a per-engagement keypair; the operator authenticates with the shared password and validates the server's certificate. This channel is encrypted and not visible to the target network (it goes over the operator's own infrastructure).
  • Team Server → Redirector: The team server does not authenticate the redirector. The redirector is configured to forward matching requests. The team server simply accepts HTTP connections from the redirector's IP; if the operator is careful, the team server's firewall allows 80/443 only from the redirector's IP.
  • Redirector → Beacon: This is the actual C2 channel. It is HTTP or HTTPS. If HTTPS, the TLS termination happens at the redirector (the beacon validates the redirector's certificate, not the team server's). The redirector may terminate TLS and forward plaintext internally to the team server, or it may pass through the TLS session.

Telemetry Exposure Per Layer

LayerVisible telemetry
Beacon → RedirectorDNS query for redirector domain; TLS ClientHello (JA3 fingerprint); HTTP/S request (URI, User-Agent, headers); connection timing; bytes transferred
Redirector → Team ServerInternal network flow (VPS-to-VPS or CDN-to-origin); only visible if IR pivots to the redirector host
Operator → Team ServerExternal to target network; not visible to target's SOC

Detection angles available to a defender at Meridian Freight: JA3 blocklist (Zeek ssl.log), domain categorization (proxy log enrichment), beacon timing analysis (NetFlow CV), URI path signature (proxy log pattern match), process-to-network anomaly (Sysmon EID 3 correlation).


Chapter 2: Beacon Check-In Mechanics

Sleep and Jitter

Every beacon has two timing parameters: sleep (seconds) and jitter (percentage). The effective sleep for each check-in cycle is computed as:

effective_sleep = sleep_seconds * (1 + random.uniform(-jitter_pct / 100, jitter_pct / 100))

Example: sleep = 60, jitter = 20. The uniform sample is drawn from [-0.20, +0.20], so the effective sleep for each cycle is drawn from [48, 72] seconds. The beacon does not check in at a perfectly regular interval — it checks in somewhere in that window.

Why jitter exists: a process that makes an outbound HTTPS connection to the same host every exactly 60.000 seconds is trivially detected by any periodic-connection algorithm. Jitter adds noise to the interval, making the connection pattern look less mechanical. However — as we will cover in the detection chapter — jitter only adds noise, not chaos. The underlying rhythm is still statistically detectable.

Typical operational values: short-haul interactive beacons use sleep=5, jitter=10; long-haul persistence beacons use sleep=3600, jitter=25 or higher.

HTTP Staging

On first contact, a beacon may need to fetch its full stage — the second-stage DLL or shellcode that contains its actual capability. This is the "stager" pattern: the initial shellcode is minimal (sometimes called a "stager" or "dropper"), and its only job is to fetch the full implant from the team server over HTTP/S and reflectively load it into memory. The staging URI is typically different from the check-in URI (e.g., /ca for staging vs /activity for check-ins in a default Cobalt Strike profile).

After staging, the beacon enters the check-in loop.

Check-In Request Structure

A typical beacon HTTP check-in looks like:

GET /api/v2/telemetry HTTP/1.1
Host: cdn-edge.examplefronted.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: */*
Cookie: __utmz=<base64-encoded-metadata>

The Cookie header (or a custom header, or the URI itself) carries the encoded beacon metadata: hostname, username, process ID, session ID, and any pending task results. The encoding is typically base64 with XOR or RC4 applied — it is not designed to be cryptographically secure (the channel itself is TLS-encrypted), just obfuscated enough to prevent casual inspection.

The server responds with either a 200 and an empty body (no pending tasks) or a 200 with a base64-encoded task in the response body. The beacon decodes the task, executes it, and on the next check-in POSTs the result back.

Task-Polling Flow

The full cycle:

  1. Beacon wakes from sleep.
  2. Beacon builds check-in request (GET or POST depending on profile).
  3. Beacon sends request to redirector; redirector forwards to team server.
  4. Team server checks for pending tasks for this beacon's session ID.
  5. If no tasks: team server returns 200 + empty body. Beacon sleeps.
  6. If tasks pending: team server returns 200 + encoded task. Beacon decodes and executes task.
  7. Beacon POST-backs the task output (if any) on the same or next check-in.
  8. Beacon sleeps for effective_sleep seconds.

From a network perspective, steps 2-3 and 7-8 are the only observable events. Every check-in is a short TCP connection (connect, TLS handshake, HTTP exchange, close). The connection duration is typically under two seconds.

Coefficient of Variation

The coefficient of variation (CV) is the primary statistical metric for beacon detection:

CV = statistics.stdev(gaps) / statistics.mean(gaps)

Where gaps is the list of inter-connection time deltas for a specific process-to-destination flow.

Why CV works:

  • Legitimate browser traffic is bursty. A user loads a page (burst of connections), sits reading for 5 minutes (silence), loads another page (burst). The gaps have enormous variance relative to their mean. CV >> 1.0 is typical.
  • A C2 beacon with 20% jitter and 60s sleep has gaps in [48, 72]. The mean is ~60, the standard deviation is ~7. CV = 7/60 ≈ 0.12. This is far below 1.0.
  • An interactive C2 session (operator actively issuing commands) has irregular gaps — sometimes 2 seconds, sometimes 30, sometimes 180 — with moderate variance. CV typically falls in [0.3, 1.0].

Classification thresholds (empirical, from RITA and academic papers):

CV rangeClassification
< 0.3Beaconing (automated, regular)
0.3 – 1.0Interactive (human-driven)
> 1.0Benign (bursty browser / application traffic)

Worked example:

Process: svchost.exe (PID 1234), destination: 192.168.1.10:443

Connection timestamps (Unix epoch seconds): [1000, 1060, 1118, 1181, 1242, 1303]

Gaps: [60, 58, 63, 61, 61]

Mean gap: 60.6

Stdev of gaps: 1.82

CV: 1.82 / 60.6 = 0.030

Classification: beaconing (CV far below 0.3 threshold).

With 20% jitter the CV would rise to approximately 0.12, still well below threshold. It takes jitter above approximately 60% before the CV crosses 0.3 — at that point the beacon's rhythm is too noisy to be reliably tracked anyway, and the operator has usually traded stealth for reliability.


Chapter 3: Redirectors and Traffic Blending

Why Redirectors Exist

The naive C2 architecture has the beacon connecting directly to the team server IP. This is operationally dangerous for three reasons:

  1. The first time any sensor logs the destination IP, IR has the team server. A single PCAP from a compromised endpoint exposes the entire engagement.
  2. The team server IP has no cover story. It is a VPS in a cloud provider's IP range, newly spun up, with no domain history, no categorization, and a self-signed TLS cert. Every proxy in the world will block it immediately.
  3. Threat intel feeds maintain databases of known C2 infrastructure. An IP used in a prior engagement will be blocklisted before this engagement begins.

The redirector solves all three problems. It is the first line of operational security on the network layer.

Apache mod_rewrite Redirector

The most common redirector implementation uses Apache's mod_rewrite module to conditionally proxy traffic to the team server. Here is a complete configuration:

RewriteEngine On

# Only forward traffic that matches expected beacon patterns
# Condition 1: User-Agent must match the configured beacon UA
RewriteCond %{HTTP_USER_AGENT} "Mozilla/5.0 \(Windows NT 10\.0; Win64; x64\)" [NC]

# Condition 2: URI must match one of the configured beacon paths
RewriteCond %{REQUEST_URI} "^/(api/v2/telemetry|cdn-cgi/image/|assets/js/)" [NC]

# Forward matching traffic to the team server (P = proxy, L = last rule)
RewriteRule ^(.*)$ http://TEAMSERVER_IP:80$1 [P,L]

# Everything else: redirect to a legitimate site (cover story)
RewriteRule ^(.*)$ https://www.microsoft.com/ [R=302,L]

The [P,L] flag causes Apache to make a server-side HTTP request to the team server and return the response to the client, transparently. From the beacon's perspective it is talking to the redirector; it never sees the team server IP. The [R=302,L] rule handles all non-beacon traffic by sending it to a plausible legitimate site — a Shodan scanner that hits the redirector sees a 302 to microsoft.com and moves on.

The mod_proxy and mod_proxy_http modules must be enabled. The ProxyPreserveHost Off directive is often set to prevent forwarding the Host header to the team server (where it would be the redirector's domain, which would mismatch the team server's listener configuration).

Nginx proxy_pass Redirector

Nginx is an alternative that some operators prefer for its performance characteristics:

server {
    listen 443 ssl;
    server_name cdn-edge.cover-domain.com;

    ssl_certificate     /etc/letsencrypt/live/cover-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cover-domain.com/privkey.pem;

    # Beacon check-in paths: proxy to team server
    location ~ ^/(api/v2/telemetry|cdn-cgi/image/|assets/js/) {
        proxy_pass http://TEAMSERVER_IP:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Everything else: redirect to cover story
    location / {
        return 302 https://www.microsoft.com/;
    }
}

Nginx terminates TLS at the redirector and forwards HTTP to the team server. This means the team server sees plaintext, which simplifies its listener configuration. It also means the TLS fingerprint the beacon presents is negotiated with the redirector's TLS stack (the Nginx server's configuration), not the team server's — allowing the operator to carefully configure Nginx's cipher suite list to avoid known-bad JA3s.

Domain Fronting

Domain fronting is a technique that abuses CDN routing to make C2 traffic appear to originate from a legitimate CDN customer's domain. It exploits the separation between the TLS Server Name Indication (SNI) extension and the HTTP Host header:

  • TLS SNI (visible to the network): The domain name the client includes in the TLS ClientHello. This is what firewalls, proxies, and IDS see as the destination domain. It is sent before TLS encryption is established.
  • HTTP Host header (visible only after TLS decryption, historically only to the CDN): The domain name in the HTTP request after the TLS session is established.

The domain fronting technique:

  1. The operator registers a C2 domain (e.g., c2.attacker.com) behind a major CDN (e.g., Cloudflare, Fastly, Azure CDN).
  2. The operator also controls knowledge of a legitimate high-reputation domain that uses the same CDN (e.g., legit-saas.com, also on Cloudflare).
  3. The beacon connects to the CDN's IP address with TLS SNI set to legit-saas.com.
  4. Inside the TLS tunnel, the beacon sends Host: c2.attacker.com.
  5. The CDN routes the request based on the Host header to the C2 origin.
  6. From the target network's perspective: traffic goes to legit-saas.com on a Cloudflare IP. Blocking it means blocking Cloudflare entirely.

Detection of domain fronting:

  • JA3 anomaly: the process making the connection (e.g., svchost.exe) should not be making TLS connections at all, regardless of destination.
  • Non-browser process to CDN IP: svchost.exe periodically connecting to 104.21.x.x (Cloudflare) is anomalous even if the SNI looks legitimate.
  • Host-header logging: CDNs have increasingly added Host-header inspection. Microsoft (Azure CDN) and Google (GCP CDN) both added SNI-Host mismatch detection around 2018-2019. Cloudflare restricts fronting on its platform. The technique's effectiveness against major CDNs has significantly degraded.
  • TLS certificate subject: if the redirector presents a certificate whose subject does not match the SNI, that mismatch is an immediate detection signal.

MITRE ATT&CK: T1090.004 (Proxy: Domain Fronting), T1090.001 (Proxy: Internal Proxy).

CDN Abuse Without Fronting

Even without fronting, operators use CDNs to blend. The C2 domain is a legitimate CDN customer with valid DNS records, a valid TLS certificate from Let's Encrypt or a CA, and plausible content. The CDN's IP reputation is excellent (major CDN IPs are rarely blocklisted because doing so breaks legitimate traffic). The URI paths are crafted to look like CDN asset requests (/cdn-cgi/image/quality=85/https://..., /assets/js/analytics.min.js).

This does not hide the TLS fingerprint or the beacon timing, but it defeats category-based blocking and IP reputation scoring.


Chapter 4: JA3/JA4 Fingerprinting Under the Hood

The TLS ClientHello

When a TLS client initiates a connection, its first message is the ClientHello. This message is sent before any encryption — it is plaintext on the wire. It contains:

  • The TLS version the client proposes (encoded as a two-byte integer; TLS 1.2 = 0x0303, TLS 1.3 = 0x0304).
  • A list of cipher suites the client supports, in the client's preferred order.
  • A list of TLS extensions the client wants to negotiate.
  • Within the supported_groups (elliptic_curves) extension: a list of elliptic curve IDs.
  • Within the ec_point_formats extension: a list of point format IDs.

Because the ClientHello is entirely determined by the TLS library the client uses (not the server, not the application), different TLS implementations produce detectably different ClientHellos. Go's crypto/tls, Python's ssl (wrapping OpenSSL), Chrome's BoringSSL, Java's JSSE, and Cobalt Strike's Java-based TLS stack all produce distinct ClientHellos.

JA3 Hash Construction

JA3 was published by Salesforce in 2017. The algorithm:

  1. Extract five fields from the ClientHello:

    • TLS version: decimal integer (e.g., TLS 1.2 → 769)
    • Cipher suites: ordered list of decimal IDs, GREASE values (RFC 8701) excluded (e.g., 49195,49199,52393,52392,49196,49200,49162,49161,49171,49172,51,57,47,53,10)
    • Extensions: ordered list of decimal type codes, GREASE excluded (e.g., 0,23,65281,10,11,35,16,5,13,28)
    • Elliptic curves: ordered list of decimal group IDs from the supported_groups extension (e.g., 29,23,24)
    • Point formats: ordered list of decimal format IDs from the ec_point_formats extension (e.g., 0)
  2. Format each field as a comma-separated list.

  3. Join all five fields with dashes:

    769,49195-49199-52393,0-23-65281,29-23-24,0
    
  4. Compute the MD5 hash of that string. That is the JA3 hash.

Realistic example (Cobalt Strike default Java TLS stack):

TLS version:    769
Cipher suites:  49162,49161,52393,49200,49199,49172,49171,157,156,61,60,53,47,10,255
Extensions:     0,65281,10,11,35
Elliptic curves: 23,24,25
Point formats:  0

JA3 string: 769,49162-49161-52393-49200-49199-49172-49171-157-156-61-60-53-47-10-255,0-65281-10-11-35,23-24-25,0
JA3 hash:   a0e9f5d64349fb13191bc781f81f42e1

That hash, a0e9f5d64349fb13191bc781f81f42e1, is the most-blocked JA3 hash in the world. Every Cobalt Strike beacon using the default Java SSL configuration produces it. Every Zeek installation with a JA3 blocklist fires on it immediately.

Why reordering ciphers changes the hash: the cipher suite list is taken in the order the client presents it in the ClientHello. If a beacon's profile reorders the ciphers (e.g., putting 47 before 53), the JA3 string changes and produces a completely different MD5. This is how operators evade JA3 blocklists — but it does not evade JA3 anomaly detection, which asks "does the JA3 hash for this Image match what we expect from its TLS library?"

JA4 Improvements

JA4 (published by FoxIO in 2023) improves on JA3 in several ways:

  • Stability: JA4 sorts cipher suites and extensions before hashing, making it stable across minor library version bumps that reorder preferences without changing capability.
  • Readability: JA4 uses a human-readable prefix: t (TCP) or q (QUIC), TLS version (13 for TLS 1.3, 12 for TLS 1.2), SNI presence (d for domain, i for IP), number of cipher suites (two digits), number of extensions (two digits), and first ALPN value.
  • Sortability: because the prefix encodes structured fields, JA4 hashes are sortable and groupable without decoding.

Example JA4 prefix for a TLS 1.3 connection with SNI, 17 cipher suites, 12 extensions, and h2 ALPN: t13d1712h2.

JA4 also defines sub-hashes (JA4_r for the raw sorted cipher list, JA4_s for the server-side equivalent) to enable more nuanced fingerprinting.

Detection Use in Practice

Zeek ssl.log fields: ts, uid, id.orig_h, id.orig_p, id.resp_h, id.resp_p, version, cipher, ja3, ja3s, subject, issuer.

Known-bad JA3 blocklist approach: maintain a list of JA3 hashes for default C2 framework configurations (Cobalt Strike default, Metasploit default, Sliver default, Empire default). Alert on any SSL connection whose ja3 field matches. This is a high-precision, low-recall detection — it catches operators who haven't customized their profiles.

JA3 anomaly scoring approach: for each process that makes TLS connections, baseline the expected JA3 hash based on the process name and TLS library it links against (chrome.exe → BoringSSL JA3, python.exe → OpenSSL JA3). Alert when a process's observed JA3 doesn't match its expected set. This catches novel C2 frameworks and operators who change their JA3 to avoid the known-bad list but produce a JA3 that doesn't match any known legitimate TLS library.


Chapter 5: Malleable Profiles

What a Malleable Profile Is

A malleable profile (Cobalt Strike's term, but the concept applies to any configurable C2 framework: Sliver, Havoc, Mythic, BRC4) is a configuration file that controls every externally-observable characteristic of the beacon's network communication. It is the operator's complete description of what "normal traffic" the beacon should impersonate.

The profile does not change what the beacon does — it only changes what the beacon looks like on the wire. Commands, task execution, and output collection are unchanged. What changes is the HTTP method, URI paths, headers, body encoding, sleep/jitter values, named pipe suffix, and TLS certificate subject.

In Cobalt Strike, profiles are .profile files loaded at team server startup. Changes take effect for new listeners; existing beacons continue using their original profile configuration.

The Five Observable Categories

1. Named pipe (SMB beacon) SMB beacons use Windows named pipes for intra-host communication (e.g., lateral movement from host A to host B via SMB). The named pipe name is created by the implant and must be opened by a connecting payload. The default Cobalt Strike named pipe pattern is \\.\pipe\msagent_ followed by a random hex suffix. Sysmon EID 17 (PipeCreated) and EID 18 (PipeConnected) log named pipe creation and connection events including the pipe name. The default pattern is in every EDR signature database.

A custom profile uses an innocuous-looking pipe name that matches legitimate Windows services (e.g., \\.\pipe\chrome.6729.8.1579677454, mimicking Chrome's pipe naming convention).

2. User-Agent The User-Agent string appears in every HTTP check-in. Proxies log it. The default Cobalt Strike user-agent is:

Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)

This is Internet Explorer 8 on Windows XP. In 2024, this user-agent string has two immediate problems: (1) IE8 was end-of-life in 2016, (2) Windows XP was end-of-life in 2014. No legitimate traffic uses this string on a modern enterprise network. Every proxy in the world flags it.

A custom profile uses a plausible current browser UA matching the target environment:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36

3. URI paths The URIs used for staging and check-in appear in proxy logs and are matched by Snort/Suricata rules. Default Cobalt Strike URIs include /ca (staging), /activity, /push, /admin/get.php. These appear in published Snort rule sets and are matched by commercial NGFWs.

A custom profile uses URIs that blend with the cover domain: /api/v2/telemetry, /cdn-cgi/image/quality=85/, /assets/js/analytics.min.js. These look like legitimate CDN or SaaS API traffic.

4. Sleep and jitter Default Cobalt Strike sleep is 60 seconds with 0% jitter. Zero jitter produces a CV of exactly 0.0 — the most trivially-detected beacon timing pattern possible. A well-configured profile sets jitter to 20-30% to raise the CV above 0.05.

5. TLS certificate subject If the beacon uses HTTPS and the listener presents a TLS certificate, that certificate's subject is logged in Zeek ssl.log. The default Cobalt Strike certificate subject is:

O=Major Institutions Inc., L=Springfield, ST=Ohio, C=US

This subject is in every threat intel feed. A custom profile uses a certificate whose subject matches the cover domain, issued by a public CA (Let's Encrypt or a DV CA), so it looks like a legitimate HTTPS service.

Why Defaults Are Catastrophic

Each default indicator is independently detectable. If an operator uses a default profile, they face five simultaneous detection opportunities:

  • Named pipe string match (Sysmon EID 17)
  • User-agent string match (proxy log)
  • URI path match (Snort/Suricata rule)
  • JA3 hash match (Zeek ssl.log blocklist)
  • TLS certificate subject match (Zeek ssl.log)

A single custom profile that addresses all five eliminates all five string-match detections simultaneously. It does not eliminate timing-based or process-anomaly detections — those require different mitigations (jitter tuning and process injection into expected network-connected processes).

Detection of Custom Profiles

Even with all default indicators removed, a custom profile leaves residual detection surface:

  • JA3 anomaly (process whose JA3 doesn't match its expected TLS library)
  • Beacon timing (CV below threshold, even with jitter)
  • Process-to-network anomaly (non-browser process making periodic HTTPS connections)
  • Bytes-out spike during exfil (anomalous outbound data volume on an otherwise-small-payload flow)
  • Domain age (newly registered domains are suspicious regardless of category)

A mature operator addresses all of these; a novice operator addresses only the string-match detections and remains caught by statistical detection.


Chapter 6: Detection Engineering for C2

Detection Class 1: NetFlow Beacon Timing

The workflow for production beacon-timing detection:

  1. Collect NetFlow or IPFIX records from all internal-to-external flows.
  2. Enrich with process name from endpoint telemetry (Sysmon EID 3 correlation by source IP and source port).
  3. Group flows by (src_ip, dst_ip, dst_port, process_name).
  4. For each group with at least 3 connections, sort by timestamp, compute inter-connection gaps.
  5. Compute CV = stdev(gaps) / mean(gaps).
  6. Alert if: CV < 0.3 AND connection_count >= 3 AND median_gap < 3600 seconds.
  7. Enrich the alert with process name, destination domain (PTR lookup), domain age, and JA3 hash.

Threshold rationale: CV < 0.3 catches beacons up to approximately 45% jitter. The median_gap < 3600 filter excludes very-long-sleep persistence beacons (which should be caught by the DNS-based RITA detection instead). The connection_count >= 3 minimum prevents false positives from short-lived bursty connections.

Detection Class 2: JA3 Anomaly — Sigma Rule

title: Suspicious JA3 Fingerprint — Known C2 Default
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
status: experimental
description: Detects TLS connections using JA3 fingerprints associated with default C2 framework configurations
references:
  - https://github.com/salesforce/ja3
logsource:
  category: network
  product: zeek
detection:
  selection:
    ja3|contains:
      - 'a0e9f5d64349fb13191bc781f81f42e1'
      - '6bca5b8d3c9f3f8a3c9c7e5f2d1b4a8e'
  condition: selection
falsepositives:
  - Legacy applications using old TLS libraries
level: high
tags:
  - attack.command_and_control
  - attack.t1071.001

For JA3 anomaly detection (not just blocklist), the Sigma rule would add a filter section excluding known-good JA3 hashes for the process's expected TLS library, combined with a process_name field from endpoint enrichment. The alert fires when process_name = svchost.exe AND ja3 is not in the known-good set for system processes.

Detection Class 3: Process-to-Network Anomaly

Non-browser processes making periodic HTTPS connections to CDN IPs are a high-fidelity signal. The detection logic using Sysmon EID 3:

EID 3 (NetworkConnect) WHERE:
  Image NOT IN (expected_network_processes)        # e.g., chrome.exe, firefox.exe, update services
  AND DestinationPort == 443
  AND DestinationIp IN (cdn_ip_ranges)             # Cloudflare, Akamai, Fastly, Azure CDN
  AND connection is periodic (CV < 0.3 over 3+ events)

Sysmon EID 3 fields used in this detection:

FieldUse
UtcTimeTimestamp for gap computation
ProcessGuidStable process identifier across PID reuse
ProcessIdFor correlation with EID 1 (process creation)
ImageFull process path — used to classify expected vs anomalous network users
Protocoltcp for C2; udp for DNS beaconing
Initiatedtrue = outbound connection; only outbound matters for C2
SourceIpInternal host IP
SourcePortEphemeral port
DestinationIpRedirector or CDN IP
DestinationPort443 for HTTPS C2
DestinationHostnameRedirector domain (if DNS resolution is available)

Detection Class 4: Zeek conn.log Analysis

Zeek's conn.log provides per-flow metadata without payload inspection. Fields for beacon detection:

FieldTypeUse
tstimestampFlow start time — for gap computation
id.orig_haddrSource IP
id.resp_haddrDestination IP
id.resp_pportDestination port
protoenumtcp / udp
servicestringProtocol detected (ssl, http, dns)
durationintervalFlow duration — C2 check-ins are very short
orig_bytescountBytes from client — task results POSTed back
resp_bytescountBytes to client — response payload
conn_statestringConnection state (SF = successful, S0 = no reply)

Beacon detection from conn.log:

# Pseudocode
flows = read_zeek_conn_log("conn.log")
by_dest = group_by(flows, key=lambda f: (f.resp_h, f.resp_p))

for dest, flow_list in by_dest.items():
    sorted_flows = sort_by(flow_list, key="ts")
    gaps = compute_gaps(sorted_flows, key="ts")
    if len(gaps) >= 2:
        cv = stdev(gaps) / mean(gaps)
        if cv < 0.3 and median(gaps) < 3600:
            alert(dest, cv, median(gaps), len(gaps))

The combination of short duration (under 2 seconds), small resp_bytes (empty task response), and periodic timing is highly specific to C2 beaconing. No legitimate application traffic has all three properties simultaneously.


Chapter 7: OPSEC Hygiene

OPSEC failures in C2 operations fall into four categories. Each failure has a specific telemetry fingerprint that an IR analyst can pivot on.

Infrastructure OPSEC

Domain age. Registering a C2 domain on the day of the engagement produces a domain with age = 0 days. Palo Alto DNS Security, Cisco Umbrella, and Infoblox BloxOne all score newly registered domains (NRDs) as high-risk and apply additional scrutiny or automatic blocking for the first 30 days. Mitigation: register domains weeks before the engagement and pre-age them with passive DNS traffic.

IP reuse across engagements. If the same team server IP (or redirector IP) appears in two engagements, PDNS (passive DNS) ties them together. An IR team investigating engagement B can pivot to engagement A's victims by looking up all domains that ever resolved to the known IP. Mitigation: spin up fresh infrastructure (new IPs, new accounts) for every engagement.

Direct team server exposure. If the beacon config contains the team server IP (i.e., no redirector is used), one memory dump from the victim host exposes the team server. Every beacon config is readable from the implant's memory by a skilled analyst. Mitigation: always use a redirector; never put the team server IP in a beacon config.

Expired or self-signed TLS certificate. A self-signed cert on a redirector is immediately flagged by Zeek ssl.log (issuer = subject, no CA chain) and by Bro-based cert-transparency checks. An expired cert triggers browser warnings that may appear in proxy logs. Mitigation: use Let's Encrypt certificates, renewed automatically.

Profile OPSEC

Default named pipe. Sysmon EID 17 fires on every named pipe creation. The default Cobalt Strike pipe pattern msagent_ followed by hex is in every commercial EDR's signature set. Detection is a simple string match — no heuristics needed. Mitigation: configure a custom pipe name in the SMB listener settings, matching a legitimate Windows service pipe name pattern.

Default user-agent. The IE8-on-XP user-agent is the most-detected C2 indicator in proxy logs. It appears in Snort rules (alert tcp any any -> any 80 (content:"MSIE 8.0"; content:"Windows NT 5.1";). Every proxy with a UA-anomaly rule flags it. Mitigation: use a current browser UA matching the target fleet.

Default JA3. a0e9f5d64349fb13191bc781f81f42e1 is in every JA3 blocklist. Mitigation: configure the TLS library to use a different cipher order, or wrap the beacon in a custom TLS transport.

Zero jitter. Jitter of 0% produces effective_sleep = sleep on every cycle, giving a CV of 0.0. This is the single most-detectable beacon timing pattern — even a simple "count periodic connections" rule catches it. Mitigation: set jitter to at least 20%.

Default URIs. /ca, /push, /activity, /admin/get.php are in Snort, Suricata, and Zeek Intel Framework signatures. Mitigation: configure URI paths that match a CDN or SaaS API pattern relevant to the cover domain.

Operational OPSEC

Business-hours-only beaconing. If the beacon sleeps during off-hours (or the operator configures a schedule mask that inhibits check-ins outside 9-5), the resulting NetFlow pattern shows periodic connections from 9:00 to 17:00 and nothing outside those hours. This on/off pattern is more suspicious than always-on beaconing, because no legitimate background service has that schedule. Mitigation: if using schedule masks, blend the edges; or use long-sleep persistence beacons that check in once every few hours at any time.

Exfil volume spike. A beacon that normally transfers 200 bytes per check-in and then transfers 50 MB in a single POST is an anomalous bytes_out spike. Any NetFlow anomaly detector with a per-flow bytes baseline will fire. Mitigation: chunk large exfil into small transfers spread across multiple check-in cycles, or use a dedicated exfil channel (DNS, ICMP) that doesn't appear in the same flow.

Staging from C2 IP. If the stager fetches the second-stage payload from the same IP as the check-in listener, proxy logs show the same IP appearing in both the staging HTTP GET and subsequent check-ins. IR analysts look for "first-seen" IPs and cross-reference them — a staging server that reappears as a C2 server is an immediate correlation. Mitigation: use separate infrastructure for staging vs. check-in.

Inverted jitter pattern. Some operators configure short sleep during off-hours ("checking if operator is available") and long sleep during business hours. This inverted jitter pattern is detectable: NetFlow shows high-frequency connections at night and low-frequency during the day — the opposite of every legitimate application's traffic pattern.


Chapter 8: Misconceptions

Misconception 1: Domain fronting is undetectable because the network only sees traffic to a legitimate CDN.

Reality: CDNs can inspect both the TLS SNI and the HTTP Host header after TLS termination. Microsoft (Azure CDN) and Google (GCP CDN) both implemented SNI-Host mismatch blocking around 2018-2019. Cloudflare explicitly prohibits domain fronting in its Terms of Service and has technical controls to detect it. Additionally, even if the CDN does not block it, a non-browser process (e.g., svchost.exe) making periodic HTTPS connections to a CDN IP is anomalous regardless of what domain the SNI names. Host-header logging at the CDN layer, if obtained via legal process, exposes the real C2 destination.


Misconception 2: High jitter makes a beacon undetectable by timing analysis.

Reality: Jitter adds noise to the inter-connection interval but does not change the underlying rhythm. An analyst needs only enough samples to compute a stable coefficient of variation. With 10 check-ins and 30% jitter (sleep=60, gaps in [42, 78]), the mean gap is ~60, the stdev is ~10, and the CV is ~0.17 — still well below the 0.3 detection threshold. To push CV above 0.3, jitter must exceed approximately 45-50%, at which point the beacon's intervals become so unpredictable that the operator loses reliable check-in timing. The tradeoff between stealth and reliability makes very high jitter impractical for active operations.


Misconception 3: JA3 blocklists are easy to evade and therefore useless as a detection.

Reality: A static JA3 blocklist (matching only known-bad hashes) is indeed easy to evade by reordering cipher suites. However, the more powerful defense is JA3 anomaly detection: baseline the expected JA3 hash for each process based on its known TLS library, and alert when a process produces an unexpected hash. Reordering ciphers to evade the known-bad list produces a new hash — but that new hash also doesn't match any known legitimate TLS library, so it fires the anomaly detector instead. The attacker's evasion of the blocklist directly triggers the anomaly alert.


Misconception 4: Using HTTPS means the SOC cannot see C2 traffic without decrypting it.

Reality: HTTPS hides the payload but not the metadata. Without decryption, a network sensor can observe: the destination IP and port, the TLS SNI (destination domain name, in plaintext in the ClientHello), the JA3 fingerprint (TLS library fingerprint, in plaintext in the ClientHello), the connection timing (intervals between flows), the bytes transferred per flow (in NetFlow records), the TLS certificate subject and issuer (sent by the server in plaintext during the handshake), and whether the certificate matches the SNI. The payload — the task and its result — is encrypted. But everything needed to detect beaconing (timing, fingerprint, process, destination) is available without decryption.


Misconception 5: A redirector completely protects the team server from discovery.

Reality: A redirector hides the team server IP from the beacon's config and from casual network observation. It does not make the team server invisible to a determined IR team. Several pivot paths exist: (1) if the redirector is seized, its configuration (proxy_pass rule) reveals the team server IP; (2) if the team server and redirector share an ASN, SSH host key, or TLS certificate serial number, IR can correlate them via Shodan or passive DNS; (3) the team server's operator-facing port (50050) must be accessible from the operator's infrastructure — exposure of that port to any scanner reveals the team server regardless of redirector configuration. The redirector buys time and forces the IR team to pivot, but it is not an absolute protection.


Misconception 6: Sleeping for 24 hours between check-ins defeats all beacon detection.

Reality: Very long sleep intervals shift the primary detection surface from NetFlow to DNS. A beacon that sleeps for 24 hours must still resolve its C2 domain's DNS periodically (DNS records TTL and client cache expiration force re-resolution roughly daily). Tools like RITA (Real Intelligence Threat Analytics from Active Countermeasures) specifically target long-sleep DNS beaconing by analyzing periodic DNS query patterns. A domain that a single host resolves every 24 hours ± small jitter, consistently over days or weeks, is statistically distinct from legitimate DNS traffic. Long-sleep beacons also remain dormant on the host for extended periods, during which endpoint forensics (memory analysis, process listing) may detect the implant independently of network signals.


Lab Walkthrough

Lab 01 — Beacon Rhythm Analyzer

The lab implements five functions over a data structure representing synthetic NetFlow connection records. Each record has a timestamp (Unix epoch seconds), src_ip, dst_ip, dst_port, and process_name.

Function 1: beacon_interval(connections)

  • Sort connections by timestamp (do not assume input is sorted).
  • Compute the list of inter-connection gaps: gaps[i] = connections[i+1].timestamp - connections[i].timestamp.
  • Return statistics.median(gaps).
  • Why median rather than mean: outliers (a delayed check-in due to sleep jitter or host resource contention) pull the mean away from the true beacon interval. The median is robust to a small number of anomalous gaps.
  • Edge case: if fewer than 2 connections are provided, return 0.0.

Function 2: jitter_coefficient(connections)

  • Sort by timestamp, compute gaps.
  • Return statistics.stdev(gaps) / statistics.mean(gaps) (this is the CV).
  • Edge case: if fewer than 2 gaps exist (i.e., fewer than 3 connections), stdev is undefined; return 0.0.
  • Note: statistics.stdev in Python requires at least 2 data points; guard for this.

Function 3: classify_rhythm(connections)

  • Compute CV using the same gap list.
  • Return "beaconing" if CV < 0.3.
  • Return "interactive" if 0.3 <= CV < 1.0.
  • Return "benign" if CV >= 1.0.
  • Edge case: if fewer than 3 connections, return "benign" (insufficient data to classify).

Function 4: anomalous_processes(all_connections, expected_processes)

  • Input: a list of all connection records (mixed processes), and a set of expected process names (e.g., {"chrome.exe", "svchost.exe"} — the NON-anomalous ones to exclude).
  • Group connection records by process_name.
  • For each process group: compute CV; if CV < 0.3 AND the process is NOT in expected_processes AND connection count >= 3, add it to the anomalous list.
  • Return the list of anomalous process names.
  • Key insight: the grouping step is per-process, not per-flow. All connections for notepad.exe across all destinations are grouped together for classification.

Function 5: detection_for_connection(connection, cv, expected_processes)

  • Given a single connection record, its pre-computed CV, and the expected process set:
  • Return a list of detection strings. Include "BEACON_TIMING" if CV < 0.3, "PROCESS_ANOMALY" if process not in expected_processes, "PORT_ANOMALY" if dst_port not in {80, 443}.
  • Multiple detections can fire simultaneously.

Expected test results:

  • REGULAR_BEACON fixture (60s sleep, 5% jitter, 6 connections): classified as "beaconing", CV ≈ 0.03.
  • INTERACTIVE_PATTERN fixture (variable gaps 2-180s, 8 connections): classified as "interactive", CV ≈ 0.6.
  • BENIGN_LONG fixture (bursty browser-like connections with long silences, 10 connections): classified as "benign", CV > 1.0.

Lab 02 — C2 Profile Analyzer

The lab implements four functions over a profile dictionary with keys: user_agent, uri_paths (list), sleep_seconds, jitter_pct, named_pipe, cert_subject, jitter_pct, and optional cdn_fronting (bool).

Default constants are provided: DEFAULT_USER_AGENT, DEFAULT_URIS (list), DEFAULT_NAMED_PIPE, DEFAULT_CERT_SUBJECT, DEFAULT_JA3.

Function 1: profile_noise_score(profile)

  • Iterate over the five default indicator categories.
  • Add 1 point for each default indicator present: user-agent matches default, any URI in uri_paths matches a default URI, named pipe matches default, cert subject matches default, jitter is 0%.
  • Add 1 more point if jitter_pct == 0.
  • Maximum score is 6 (all five defaults present plus zero jitter).
  • Return the integer score.

Function 2: redirector_gaps(profile)

  • Analyze the profile for missing redirector hardening.
  • Return a list of gap strings. Include "no_cdn_fronting" if profile.get("cdn_fronting") != True, "default_uris" if any profile URI matches a default, "no_tls_cert" if cert subject is default or missing.
  • The list represents gaps that an IR analyst could exploit to pivot to the team server.

Function 3: detection_opportunities(profile)

  • Map each noisy indicator to its detection method.
  • Return a list of detection-opportunity dicts: {"indicator": "user_agent", "detection": "proxy_log_ua_string_match", "rule": "snort_ua_rule"}.
  • Cover all five observable categories plus JA3 (always present as an opportunity).

Function 4: best_mitigation(profile)

  • Return the single highest-impact mitigation recommendation as a string.
  • Priority order: if JA3 is default → "Customize TLS cipher order to change JA3 hash". Else if jitter is 0 → "Set jitter to at least 20% to raise beacon CV above 0.3". Else if URIs are default → "Replace default URIs with CDN-style paths". Else if user-agent is default → "Replace IE8 user-agent with current Chrome UA". Else if no CDN → "Configure CDN fronting to blend with legitimate traffic". Else → "Profile is well-configured; verify domain age and cert subject".

Expected test results:

  • NOISY_PROFILE (all defaults, jitter=0): score = 6, best_mitigation returns JA3 advice.
  • CLEAN_PROFILE (all custom, jitter=25): score = 0, no detection opportunities from defaults.
  • PARTIAL_PROFILE (custom UA and URIs, but default pipe and jitter=0): score = 2, best_mitigation returns JA3 or jitter advice.

Success Criteria

You have completed this phase when:

  • LAB_MODULE=solution pytest -q passes for both labs with 0 failures.
  • You can draw the C2 architecture diagram from memory without notes, including which port each component uses and what telemetry each connection exposes.
  • You can explain the JA3 hash construction formula — the five fields, their order, how they are joined, and what is hashed — without looking it up.
  • You can state the CV threshold for beaconing (0.3) and explain why that threshold works (the statistical difference between bursty legitimate traffic and jittered periodic traffic).
  • You can list at least four default Cobalt Strike profile indicators (user-agent, named pipe pattern, URIs, JA3 hash, cert subject) without notes.
  • You can write the beacon timing detection logic in pseudocode: group by flow, compute gaps, compute CV, threshold at 0.3.
  • You can name the Zeek log fields used in beacon timing analysis: ts, id.orig_h, id.resp_h, id.resp_p, duration, orig_bytes, resp_bytes.
  • You can explain the difference between a JA3 blocklist and a JA3 anomaly detector, and why the anomaly detector catches novel C2 frameworks.

Common Mistakes and OPSEC Failures

Timing Mistakes

Setting jitter to 0. This produces a coefficient of variation of exactly 0.0. Every beacon detection tool in existence catches it in three check-ins. Even a simple spreadsheet analysis of NetFlow gaps will find it. There is no operational reason to use zero jitter — the beacon check-in time is not so precise that jitter interferes with operations.

Using very short sleep (under 10 seconds). A beacon checking in every 5 seconds generates 720 connections per hour to the same destination. IDS volume rules fire immediately. The connection rate itself is anomalous for any non-streaming application. Short-sleep beacons should only be used in environments where you have already established that network volume rules are not in place, and only for the duration of an active operation — not as a persistent implant sleep value.

Beaconing only during business hours. A schedule mask that blocks check-ins outside 09:00-17:00 creates an obvious on/off pattern in NetFlow. The SOC sees consistent hourly traffic from a process during work hours and zero traffic at night — which is the opposite of legitimate background services (Windows Update, antivirus, telemetry all run at night). If you must use a schedule, set the edges to blur across midnight and use very long sleep during restricted periods rather than a hard cutoff.

Profile Mistakes

Forgetting to change the named pipe. The named pipe is the most-forgotten observable because it is only relevant for SMB lateral movement, which happens later in the engagement after initial access is established. Operators who customize HTTP profiles sometimes forget the SMB listener entirely. Sysmon EID 17 fires on every named pipe creation — the default pipe name produces an immediate high-confidence alert on any endpoint with Sysmon deployed.

Keeping the default IE8 user-agent. This is the single most common profile mistake and the most detected C2 indicator in proxy logs. The string "MSIE 8.0" combined with "Windows NT 5.1" in a user-agent field triggers rules on every major proxy platform (Zscaler, Symantec WSS, Bluecoat, Palo Alto URL filtering). It is a higher-confidence indicator than the JA3 hash, because JA3 requires Zeek deployment while UA logging requires only a basic proxy.

Using URIs from the default profile. The URI /ca (Cobalt Strike's default staging path) appears in Snort rule 2016467 and multiple commercial NGFW signature sets. The URI /push appears in similar rules. Using default URIs alongside a custom user-agent achieves nothing — the URI match fires independently. Both must be changed.

Infrastructure Mistakes

Pointing the beacon directly at the team server IP. In a post-incident investigation, memory forensics on the compromised host extracts the beacon's embedded C2 configuration. If the config contains the team server IP, that IP is immediately pivoted: the IR team queries Shodan, PDNS, and threat intel feeds for that IP, identifies all other domains that have ever resolved to it, and potentially identifies other compromised organizations sharing the same team server. This is one of the highest-impact OPSEC failures possible.

Not configuring CDN fronting or a proper redirector. A beacon connecting directly to a VPS IP in 185.x.x.x (a common Eastern European hosting range) with no CDN cover will be blocked by most enterprise proxies on first connection, because the IP has no reputation, the domain (if any) is newly registered, and there is no categorization. The engagement stalls before it begins. At minimum, a VPS-based redirector with a pre-aged domain and valid TLS certificate is required; CDN fronting is preferable.

Using the same TLS certificate across multiple engagements. TLS certificates have a serial number that is unique to each issuance. Certificate transparency logs record every publicly-issued certificate. If an operator reuses a Let's Encrypt certificate (or reuses the same domain) across engagements, IR teams can use the serial number to correlate infrastructure across engagements and potentially across clients. Each engagement should use freshly issued certificates and fresh domains.

Lab Implementation Mistakes

Forgetting to sort connections by timestamp before computing gaps. The test fixtures are not guaranteed to be in timestamp order (real NetFlow data is rarely perfectly ordered). If you compute gaps on unsorted input, you will get negative gaps (where an out-of-order event appears later in the list than a later event), which produce nonsensical CV values. Always sort by timestamp as the first step.

Using statistics.mean instead of statistics.median for beacon_interval. The beacon interval should be the median, not the mean, because occasional delayed check-ins (the host was under heavy load, the network was congested) create outlier gaps that pull the mean above the true interval. The test fixture includes one delayed check-in specifically to test this: a beacon with nominal 60-second sleep that has one 180-second gap should return a median of approximately 60, not a mean of approximately 80.

Not handling fewer than 2 connections in jitter_coefficient. statistics.stdev raises StatisticsError if called with fewer than 2 data points. If fewer than 2 gaps exist (i.e., fewer than 3 connections total), the function must catch this and return 0.0. The test suite includes a single-connection fixture specifically to trigger this edge case.

In anomalous_processes, filtering by expected list before grouping instead of after. The function must group all connections by process name first, classify each group's CV, and then apply the expected_processes filter to decide whether to include the process in the anomalous list. Filtering out expected processes before grouping causes the mixed-process fixture (which has connections from both expected and unexpected processes) to incorrectly classify the data.


Interview Q&A

Q1: What is the difference between a redirector and a team server?

A: The team server is the actual C2 server — it manages operator connections on a high port (typically 50050 for Cobalt Strike), maintains state about each beacon (pending tasks, output history, beacon metadata), and dispatches tasks to implants. The redirector is a lightweight relay (often Apache or Nginx with a proxy rule) that sits between the beacon and the team server; its sole job is to forward HTTP/S requests that match expected beacon patterns to the team server, while serving cover content or redirecting everything else. The split exists for operational security: the beacon config contains only the redirector's IP or domain, so if IR discovers and burns the redirector, the team server IP remains unknown and the operator can spin up a new redirector without losing active beacons. A well-configured redirector also adds traffic blending — it serves legitimate content to scanners while proxying beacon traffic to the team server — and it presents a plausible cover identity (categorized domain, valid TLS cert) that passes basic proxy filtering.


Q2: How does JA3 fingerprinting work and what fields compose the hash?

A: JA3 inspects the TLS ClientHello packet — the first message in a TLS handshake, sent by the client before any encryption — and extracts five ordered fields: the TLS version (as a decimal integer, e.g., 769 for TLS 1.2), the cipher suites offered by the client (ordered decimal list, GREASE values excluded), the TLS extensions requested (ordered decimal list, GREASE excluded), the elliptic curve IDs from the supported_groups extension (ordered decimal list), and the elliptic curve point format IDs (ordered decimal list). These five fields are formatted as comma-separated lists within each field, then joined with dashes to produce a string like 769,49162-49161-52393,0-65281-10-11,23-24-25,0, and the MD5 of that string is the JA3 hash. The hash fingerprints the TLS library rather than just the cipher preference, because different TLS implementations (Go's crypto/tls, Python's ssl, Chrome's BoringSSL, Cobalt Strike's Java TLS stack) produce detectably distinct ClientHello structures with different extension ordering, different curve preferences, and different cipher suite ordering. Defenders use both known-bad blocklists (specific hashes for default C2 frameworks) and anomaly scoring (flagging processes whose observed JA3 doesn't match the expected hash for their TLS library) to detect C2 traffic.


Q3: What is the coefficient of variation and why does it detect beaconing?

A: The coefficient of variation (CV) is the ratio of the standard deviation to the mean of a set of inter-connection time gaps — specifically, the gaps between consecutive outbound connections from a specific process to a specific destination. Legitimate browser traffic is bursty: a user loads pages in clusters with long idle periods between sessions, producing gaps with very high variance relative to their mean (CV well above 1.0). A C2 beacon with 20% jitter and a 60-second sleep checks in every 48-72 seconds, producing gaps with a small standard deviation (approximately 7 seconds) relative to the mean (approximately 60 seconds), and therefore a CV around 0.12 — well below the detection threshold of 0.3. An analyst can compute this from NetFlow metadata without any payload inspection: group flows by (source IP, destination IP, destination port, process name), sort by timestamp, compute gaps, compute CV, and alert when CV is below 0.3 and the median gap is below 3600 seconds. The detection requires only metadata — no TLS decryption, no deep packet inspection, and no endpoint agent if NetFlow enrichment with process name is not available.


Q4: How does domain fronting work and why does it complicate attribution?

A: Domain fronting exploits the separation between the TLS Server Name Indication (SNI) extension — which names the destination domain in the TLS ClientHello and is visible to any network observer before encryption is established — and the HTTP Host header, which names the destination domain inside the encrypted TLS session and is (historically) only visible to the CDN after TLS termination. The operator configures the beacon to connect to a CDN's IP address while setting the TLS SNI to a legitimate, high-reputation CDN customer domain; inside the encrypted tunnel, the HTTP Host header names the actual C2 domain that the CDN routes to the operator's origin. From the target network's perspective, the traffic destination appears to be a legitimate CDN customer domain — blocking it would require blocking that legitimate domain and all traffic to the CDN's IP range. Attribution is complicated because the observable infrastructure (the CDN and the fronting domain) has no relationship to the actual C2 domain; an IR team must obtain CDN-side Host-header logs via legal process to trace the real destination. Major CDNs have moved to restrict or block this technique, but it remains relevant against CDNs that do not perform SNI-Host mismatch enforcement.


Q5: What Sysmon event IDs are most useful for C2 detection?

A: Sysmon EID 3 (NetworkConnect) is the primary C2 detection event — it records every outbound TCP/UDP connection made by any process on the host, including the full process path (Image), destination IP, destination port, destination hostname (if resolved), source IP, and source port, along with a ProcessGuid that is stable across PID reuse. This allows detection of non-browser processes making periodic HTTPS connections (periodic EID 3 events from notepad.exe or unexpected svchost.exe instances to port 443). Sysmon EID 17 and EID 18 (PipeCreated and PipeConnected) catch SMB beacon activity — EID 17 fires when a named pipe is created (e.g., \\.\pipe\msagent_0a1b2c3d for a default Cobalt Strike SMB beacon), and EID 18 fires when a pipe is connected, providing the process that connected to it. Sysmon EID 22 (DNSQuery) records every DNS resolution request including the queried name and the process that made the query, enabling detection of DNS-based beaconing or the periodic domain resolutions that long-sleep HTTP beacons make. Correlating EID 3 timestamps for a single ProcessGuid over time enables beacon timing analysis equivalent to the NetFlow CV approach, but with exact process-level attribution.


Q6: What makes a malleable profile "noisy" from a detection standpoint?

A: A noisy profile retains default indicators from the C2 framework's out-of-box configuration — values that every threat intel feed, EDR vendor, network signature set, and JA3 database has already fingerprinted with high confidence. The most damaging defaults are the JA3 hash (which identifies the default TLS library configuration and is trivially blocked via Zeek ssl.log), the user-agent string (IE8 on Windows XP is immediately flagged by any proxy with a UA-anomaly rule, because no legitimate enterprise endpoint has used that browser since 2016), and the default URI paths (/ca, /push, /activity) which appear in Snort, Suricata, and commercial NGFW signatures. A profile is also noisy if it has zero jitter (producing a CV of 0.0, the easiest possible beacon timing detection) or uses a self-signed certificate with the default certificate subject (Major Institutions Inc.), which Zeek logs in ssl.log and which matches a known-bad subject string. A fully custom profile must address all six observable categories simultaneously; neglecting even one default indicator allows a defender to build a reliable detection rule that fires regardless of how well the others are customized.


Q7: How would you detect C2 traffic in NetFlow data with no payload inspection?

A: Start by grouping NetFlow records by (source IP, destination IP, destination port) and computing the inter-flow time gaps for each group — the delta in seconds between consecutive flow start times. Compute the coefficient of variation (stdev(gaps) / mean(gaps)) for each group and flag groups where CV is below 0.3, the connection count is at least 3, and the median gap is below 3600 seconds. This statistical fingerprint identifies periodic beaconing without requiring payload access, TLS decryption, or application-layer visibility. Enrich the flagged flows with process attribution if endpoint telemetry is available (correlate source IP + source port from NetFlow with Sysmon EID 3 SourceIp + SourcePort to get the process name) — a non-browser process making periodic HTTPS connections is a near-certain C2 indicator. Layer in additional signals: look for anomalous orig_bytes spikes in otherwise-small-payload flows (indicating a task-response POST), flows whose destination domain was registered within the last 30 days (via DNS enrichment with domain age data), and flows whose destination IP's JA3 hash (from Zeek ssl.log) matches a known-bad hash or doesn't match any known legitimate TLS library for the observed process.


Q8: What is the difference between long-haul and short-haul beacons?

A: A long-haul beacon (also called a "low and slow" or "persistence" beacon) uses a sleep interval measured in hours to days — its operational purpose is to survive indefinitely on a compromised host with minimal network noise, maintaining a persistent foothold while the operator conducts the bulk of the engagement through a separate, more active channel. It checks in infrequently (once every 4-24 hours), transfers very little data per check-in, and is extremely difficult to detect via volume-based IDS rules or short-window NetFlow analysis; its primary detection surface is DNS periodic-query analysis (tools like RITA catch the daily DNS resolution pattern) and memory forensics. A short-haul (or "interactive") beacon has a sleep interval measured in seconds to minutes and is used when the operator needs near-real-time responsiveness — lateral movement, credential harvesting, live command execution, file staging — where a 60-minute check-in cycle would make operations impractically slow. The short-haul beacon's detection profile is the opposite: it generates many connection events in a short window, is caught by NetFlow CV analysis with fewer data points needed, but is only active during the operational window and is then killed to reduce exposure. Professional operators deploy long-haul beacons first for durable persistence, then spin up short-haul channels only for specific operational tasks, destroying the short-haul channel when the task is complete to minimize the time window during which the higher-noise signal is present on the wire.


References

  • MITRE ATT&CK T1071.001 — Application Layer Protocol: Web Protocols
  • MITRE ATT&CK T1090.004 — Proxy: Domain Fronting
  • MITRE ATT&CK T1090.001 — Proxy: Internal Proxy
  • MITRE ATT&CK T1205 — Traffic Signaling
  • MITRE ATT&CK T1571 — Non-Standard Port
  • Anderson, B., McGrew, D. (2016). "TLS Fingerprinting with JA3 and JA3S." Salesforce Engineering Blog.
  • Althouse, J. (2019). "JA3 — A Method for Profiling SSL/TLS Client Hello Messages." Black Hat USA 2019 Arsenal.
  • Cobalt Strike Malleable C2 Profile Reference — Strategic Cyber LLC documentation (fictional redacted).
  • Zeek Network Security Monitor documentation — conn.log and ssl.log field reference. https://docs.zeek.org/
  • Active Countermeasures RITA (Real Intelligence Threat Analytics) — beacon detection methodology. https://github.com/activecountermeasures/rita
  • Palo Alto Unit 42 — "Threat Brief: C2 Infrastructure Identification via JA3 Fingerprinting" (2022).
  • SANS FOR572 — Advanced Network Forensics: Threat Hunting, Analysis, and Incident Response.