Lesson 4 of 5 · 25 min read

TLS Hunting — JA3/JA4 Rarity, Certificate Metadata, and SNI Anomalies

Every TLS connection leaves a fingerprint in the handshake — before any data is encrypted. JA3/JA4, certificate metadata, and SNI together form the hunter’s TLS toolkit: enough to identify anomalous clients, attacker-generated certificates, and routing tricks — without decrypting a single byte.


JA3/JA4 Rarity Analysis

Building the JA3 Frequency Baseline

// 30-day JA3 frequency baseline
Zeek_ssl_CL
| where TimeGenerated > ago(30d)
| summarize 
    host_count = dcount(id_orig_h_s),
    conn_count = count(),
    sample_snis = make_set(server_name_s, 5)
  by ja3_s
| order by host_count desc
// Top 10: browser JA3s (Chrome, Firefox, Edge, Safari, curl)
// Long tail: unknown or rare clients → investigate

The top 10 JA3 values in most enterprises will be:

Any JA3 seen by only 1 host that isn’t in these expected categories is a hunt lead.

Rarity Hunt Query

// Find JA3 values unique to single hosts (rare clients)
Zeek_ssl_CL
| where TimeGenerated > ago(30d)
| summarize 
    host_count = dcount(id_orig_h_s),
    conn_count = count(),
    distinct_snis = dcount(server_name_s),
    sample_sni = take_any(server_name_s),
    sample_host = take_any(id_orig_h_s)
  by ja3_s
| where host_count == 1            // Only 1 internal host uses this TLS profile
| where conn_count > 50            // But they've used it many times (programmatic)
| where distinct_snis <= 3         // To few unique SNIs (not a browser)
| order by conn_count desc

A rare JA3 used >50 times by 1 host, connecting to ≤3 unique server names, is a high-priority beacon + TLS fingerprint combination.


Certificate Metadata Hunting

Even if a JA3 isn’t in any blocklist, a self-signed or freshly-generated certificate reveals attacker infrastructure.

// Hunt for suspicious certificate characteristics via x509.log
Zeek_x509_CL
| where TimeGenerated > ago(30d)
// Join to ssl.log to get associated connection context
| join kind=inner (
    Zeek_ssl_CL 
    | where local_orig_b == true and local_resp_b == false
  ) on $left.id_s == $right.cert_chain_fuids_s
| where 
    // Self-signed: issuer == subject
    (certificate_issuer_s == certificate_subject_s) or
    // Very short validity (auto-generated cert)
    (datetime_diff('day', certificate_not_valid_after_t, certificate_not_valid_before_t) < 30) or
    // Generic CN
    (certificate_subject_s contains "localhost" or certificate_subject_s contains "Default")
| project 
    TimeGenerated,
    id_orig_h_s,
    id_resp_h_s,
    server_name_s,
    ja3_s,
    certificate_subject_s,
    certificate_issuer_s,
    certificate_not_valid_before_t,
    certificate_not_valid_after_t

Key checks per certificate:

CheckCommandRed Flag
Cert agenot_valid_after - not_valid_before< 30 days or > 10 years
Self-signedissuer == subjectAlways suspicious on port 443
SAN extensionParse certificate extensionsNo SAN = old tooling
Subject CNcertificate_subject’localhost’, IP literal, generic
Serial numbercertificate_serial0, 1, or small integers

SNI Anomaly Hunting

Empty SNI Hunt

// Connections on port 443 with no SNI
Zeek_ssl_CL
| where TimeGenerated > ago(7d)
| where id_resp_p_i == 443
| where local_orig_b == true and local_resp_b == false
| where isempty(server_name_s)  // No SNI
| summarize 
    conn_count = count(),
    unique_dsts = dcount(id_resp_h_s)
  by id_orig_h_s
| where conn_count > 10
| order by conn_count desc

Empty SNI on port 443 with >10 connections is either: custom TLS library (C2 tool), direct-IP C2 (no domain), or misconfigured legacy application. Each deserves a process pivot.

SNI vs IP Mismatch (Domain Fronting Indicator)

// Find where SNI doesn't reverse-resolve to destination IP
// (Simplified — full domain fronting detection requires HTTP Host header comparison)
Zeek_ssl_CL
| where TimeGenerated > ago(7d)
| where local_orig_b == true and local_resp_b == false
| where isnotempty(server_name_s)
// Flag: SNI has different registrable domain than destination IP's PTR
// (Requires PTR lookup enrichment — shown conceptually)
| where sni_registered_domain != ptr_registered_domain
| where ptr_registered_domain in (known_cdn_domains)  // CDN IP + different SNI

More practically: look for ssl.log entries where server_name is an internal or allowed domain but the destination IP is external and not in that domain’s known IP range.