Skip to content

Zeek ssl.log & x509.log

Category: Network · Source: Zeek · Last updated: 2026-09-16

In one sentence

You can't read encrypted traffic, but the handshake is cleartext: ssl.log gives you the server name the client asked for (SNI), the TLS version and cipher, a JA3/JA4 fingerprint of the client software, and pointers to the certificate chain in x509.log — which is enough to identify C2 by its self-signed or default certificate, spot tools by their fingerprint, and separate real browsing from scripted traffic.

ssl.log fields

Field Meaning Analyst notes
ts, uid, id.* As in conn.log
version TLSv10, TLSv12, TLSv13, SSLv3, DTLSv12 TLS 1.3 encrypts the server cert → x509.log may be empty for it
cipher Negotiated cipher suite Ancient/weak suites from a modern host = old tooling
curve Key-exchange curve
server_name SNI — hostname the client asked for The "what did they connect to" field. Blank SNI = direct-to-IP or old tooling
resumed Session resumption used Beacons often resume; many full handshakes to one host is also odd
last_alert TLS alert if any handshake_failure, unknown_ca…
next_protocol ALPN (h2, http/1.1) Missing ALPN from a "browser" = not a browser
established Handshake completed F with many attempts = probing / failing C2
ssl_history (Zeek 5+) Letter history of the handshake Like conn.log history, for TLS
cert_chain_fuids / client_cert_chain_fuids File IDs of server / client cert chain → x509.log First element = leaf cert
subject / issuer Leaf cert subject / issuer (if seen) Self-signed when equal
client_subject / client_issuer Client cert (mutual TLS) Rare — VPNs, some C2
validation_status ok, self signed certificate, unable to get local issuer certificate, certificate has expired… Requires the validate-certs policy
ja3 / ja3s (policy) Client / server TLS fingerprint (MD5) Client software fingerprint
ja4 / ja4s (policy, Zeek 6+) Newer, sortable fingerprint e.g. t13d1516h2_8daaf6152771_02713d6af862
sni_matches_cert (policy) Does SNI match the cert's names? F = domain fronting, misconfig, or fake cert

x509.log fields

Field Meaning Analyst notes
ts, id / fingerprint File ID (fuid) of the cert; SHA-1/256 fingerprint Join to ssl.log cert_chain_fuids
certificate.version 3 usually
certificate.serial Serial number Default tool certs have known serials
certificate.subject CN=…,O=…,C=… Empty/garbage/CN=localhost → suspicious
certificate.issuer Who signed it = subject → self-signed
certificate.not_valid_before / not_valid_after Validity window Cert created minutes before first use, or valid for 1 or 100 years → generated by a tool
certificate.key_alg, sig_alg, key_type, key_length, exponent, curve Key details 1024-bit RSA / sha1WithRSA = old/lazy
san.dns, san.uri, san.email, san.ip Subject Alternative Names Real sites list many; C2 certs list none or one
basic_constraints.ca, path_len CA flag Leaf with ca=T is odd
host_cert, client_cert (Zeek 5+) Where the cert was seen

Quick queries

# Top SNIs from internal hosts — long tail again
zeek-cut server_name < ssl.log | sort | uniq -c | sort -n | head -40

# TLS to raw IP (no SNI) — legit CDNs rarely, C2 often
zeek-cut -d ts id.orig_h id.resp_h id.resp_p server_name version ja3 < ssl.log | awk '$5=="-"'

# Self-signed / invalid certs
zeek-cut -d ts id.orig_h id.resp_h server_name validation_status subject issuer < ssl.log | grep -v -P '\tok\t'

# Rare JA3 hashes (few clients use them) and where they go
zeek-cut id.orig_h ja3 < ssl.log | sort -u | awk '{print $2}' | sort | uniq -c | sort -n | head
zeek-cut -d ts id.orig_h id.resp_h server_name < ssl.log | grep -F '<ja3 hash>'

# Certificates valid for suspiciously short/long periods, or created just before use
zeek-cut -d ts certificate.subject certificate.issuer certificate.not_valid_before certificate.not_valid_after < x509.log \
  | awk -F'\t' '{print}' | sort -u | head

# Certs with no SAN (real sites almost always have one)
zeek-cut fingerprint certificate.subject san.dns < x509.log | awk -F'\t' '$3=="-"' | sort -u

# Pivot: one connection's cert chain
grep -F 'CHhAvVGS1DHFjwGM9' ssl.log | zeek-cut cert_chain_fuids | tr ',' '\n' | while read f; do grep -F "$f" x509.log; done | zeek-cut certificate.subject certificate.issuer certificate.not_valid_before

# Known-bad default certificates
# Cobalt Strike default: CN=Major Cobalt Strike, serial 146473198 ; Metasploit: random CNs but 'O=…' patterns; Sliver: random 'O=' words
grep -iE 'Major Cobalt Strike|146473198' x509.log
index=zeek sourcetype=zeek:ssl
| stats dc(id.orig_h) as clients dc(id.resp_h) as servers values(server_name) as sni count by ja3
| where clients <= 3
| sort - count

Groups all TLS handshakes by the client fingerprint ja3. values(server_name) lists every SNI seen with that fingerprint (deduplicated). A fingerprint used by ≤ 3 machines that talks to one or two servers thousands of times is a beacon; browsers produce fingerprints shared by the whole fleet.

index=zeek sourcetype=zeek:ssl validation_status!="ok" validation_status!="-"
| stats count min(_time) as first max(_time) as last by id.orig_h, id.resp_h, server_name, validation_status, subject
| convert ctime(first) ctime(last)

min(_time)/max(_time) give first and last time each internal host hit each untrusted certificate; convert ctime() turns the epoch numbers into readable dates. Sort by count to see which self-signed cert is being visited on a schedule.

Analysis tips

The cheap C2 checklist for one ssl.log row

No SNI or SNI is a raw IP · validation_status ≠ ok · subject = issuer · cert valid from today · no SAN · next_protocol blank · JA3 unique to this host · id.resp_p 443 on a bare VPS IP · connection repeats at a fixed interval. Three or more ticks → treat as C2 until proven otherwise.

  • TLS 1.3 hides the server certificate. For 1.3 you keep SNI, JA3/JA4, ALPN and timing, but x509.log won't have the server cert (unless a decrypting proxy). Encrypted SNI/ECH removes even the name — then you're down to JA4, destination IP reputation and behaviour.
  • JA3 collisions: many tools share fingerprints (Go's TLS stack, .NET, Python requests). Use JA3 to cluster and rare-list, not as a signature by itself; JA4 splits better (t13d… shows version, ALPN, cipher count). Also fingerprint the server (ja3s/ja4s) — a C2 framework's listener answers the same way to every implant.
  • Domain fronting: SNI says a.legit-cdn.com, the inner Host (invisible) says otherwise. Signs: sni_matches_cert = F, SNI on a CDN with unusually regular traffic, resp_h in a CDN range hit by exactly one host on a schedule.
  • Let's Encrypt is not a green flag: modern C2 (Sliver, Mythic, Havoc, redirectors) uses valid LE certs. Then fall back to behaviour: interval, byte sizes, destination age, JA3 rarity.
  • Legit self-signed: printers, IoT, internal admin panels, dev boxes. Filter local_resp = T first, then look at what's left going to the internet.
  • Client certs (client_subject) in outbound traffic from a workstation → VPN, corporate proxy, or a C2 using mutual TLS (rare, but Sliver mTLS mode exists).
  • Version/cipher outliers: a single host negotiating TLSv10 with TLS_RSA_WITH_RC4_128_SHA in 2026 is running something very old — or a deliberately downgraded tool.
  • Certificate reuse across IPs: the same x509 fingerprint appearing on several id.resp_h over time = the actor moves infrastructure but keeps the cert. Great pivot for threat-intel and for retro-hunting.

Correlation

Question Then look at
How much moved, how often? conn.log uid → bytes, duration; interval analysis (Beaconing & C2)
What did the name resolve from? dns.log query = server_name moments earlier
Which process? Sysmon 3 (with DestinationPort 443) on the host; EDR network telemetry
Was the cert used elsewhere? x509.log fingerprint across time; Censys / Shodan / crt.sh for the same fingerprint or serial
Alerts? Suricata tls.sni, JA3 rules (ja3.hash), notice.log SSL::Invalid_Server_Cert
Payload Not available without decryption — pivot to host artefacts (Prefetch, SRUM, memory)

References