Chapter 46

Zeek Core Logs

Zeek generates over 30 different log files, each focused on a specific protocol or category. The five most forensically significant are conn.log, dns.log, ssl.log, http.log, and files.log. Understanding the schema of each — what every field means and which fields are most useful for threat hunting — is essential for writing effective Zeek-based detection queries.

Scenario

An analyst is hunting for lateral movement across a 3-week Zeek dataset. They start with conn.log to identify hosts making many connections to internal port 445 (SMB), then cross-reference with kerberos.log to see which accounts authenticated to those destinations, then check ssl.log to see if any connections use self-signed certificates. Each log provides a different dimension of the same events — the combination paints a complete picture without touching PCAP at all.

conn.log Reference

  conn.log Field Reference (most forensically useful)
  ═══════════════════════════════════════════════════════════════════

  ts            Timestamp (Unix epoch, float)
  uid           Unique connection ID — use this to correlate across logs
  id.orig_h     Source IP
  id.orig_p     Source port
  id.resp_h     Destination IP
  id.resp_p     Destination port
  proto         Transport protocol (tcp/udp/icmp)
  service       Detected application protocol (http/dns/ssl/smtp/...)
                "-" if not detected

  duration      Session duration in seconds (NULL if incomplete)
  orig_bytes    Bytes sent by originator (payload only, not headers)
  resp_bytes    Bytes sent by responder

  conn_state    Connection state (critical for analysis):
    S0  SYN seen, no SYN-ACK (scan/blocked)
    S1  SYN+SYN-ACK, no FIN (established, still open at log time)
    SF  SYN+FIN both seen (normal completed connection)
    REJ Connection rejected (RST after SYN)
    S2  Established, originator FIN, no responder FIN
    S3  Established, responder FIN, no originator FIN
    RSTO Established, RST from originator
    RSTR Established, RST from responder
    SHR SYN-ACK with no prior SYN (partial capture)
    OTH No SYN (packet capture started mid-connection)

  missed_bytes  Bytes missed due to packet loss (gaps in capture)
  history       Brief sequence of flags: S=SYN, A=ACK, D=data, F=FIN, R=RST
                ShADadfFR means typical established connection with data
  orig_pkts     Packet count from originator
  resp_pkts     Packet count from responder

ssl.log Reference

  ssl.log Field Reference
  ═══════════════════════════════════════════════════════════════════

  ts              Timestamp
  uid             Connection UID (links to conn.log)
  id.orig_h/p     Source IP/port
  id.resp_h/p     Destination IP/port
  version         TLS version negotiated (TLSv12, TLSv13, etc.)
  cipher          Selected cipher suite
  curve           Named elliptic curve (if ECDHE)
  server_name     SNI from ClientHello (what client thinks it's connecting to)
  resumed         true if session resumed (no full handshake)
  last_alert      Alert type if connection failed
  next_protocol   ALPN value (h2, h3, http/1.1, xmpp, etc.)
  established     true if TLS handshake completed
  ssl_history     Like conn.log history but for TLS events

  subject         Certificate subject (CN=...)
  issuer          Certificate issuer
  client_subject  Client certificate subject (if mTLS)
  client_issuer   Client certificate issuer (if mTLS)

  validation_status  ok / self signed certificate / ... (cert validation result)
  ocsp_status       stapled OCSP status

  ja3             JA3 client fingerprint (MD5 hash)
  ja3s            JA3S server fingerprint (MD5 hash)

Threat Hunting Queries

bashzeek-hunt-queries.sh
#!/bin/bash
LOG_DIR="/opt/zeek/logs/current"

echo "=== 1. Large external data transfers (exfil candidates) ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/conn.log" | \
  awk -F'\t' '
    $9 ~ /[0-9]/ && $9+0 > 10000000 {  # orig_bytes > 10 MB
      # Skip private IP ranges
      if ($5 !~ /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/)
        print $1"\t"$3"\t"$5"\t"$9"\t"$10
    }
  ' | sort -t$'\t' -k4 -rn | head -20

echo ""
echo "=== 2. Internal hosts connecting to many unique external IPs (scanning or C2 pivot) ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/conn.log" | \
  awk -F'\t' '$5 !~ /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/ {print $3"\t"$5}' | \
  sort -u | awk -F'\t' '{print $1}' | sort | uniq -c | sort -rn | head -20

echo ""
echo "=== 3. Self-signed TLS certificates ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/ssl.log" | \
  awk -F'\t' '$NF ~ /self signed/ || $(NF-1) ~ /self signed/ {
    print $1"\t"$3"\t"$5"\t"$8"\t"$NF
  }' | head -20

echo ""
echo "=== 4. TLS connections with no SNI ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/ssl.log" | \
  awk -F'\t' '$7 == "-" && $6 == "443" {print $3"\t"$5}' | \
  sort | uniq -c | sort -rn | head -20

echo ""
echo "=== 5. Hosts making DNS queries for high-entropy domains ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/dns.log" | \
  awk -F'\t' '$10 == "1" {print $9}' | \   # qtype 1 = A record
  sort | uniq | python3 -c "
import sys, math
from collections import Counter

def entropy(s):
    c = Counter(s)
    total = len(s)
    return -sum((v/total)*math.log2(v/total) for v in c.values())

for line in sys.stdin:
    name = line.strip()
    parts = name.split('.')
    if len(parts) >= 2:
        sld = parts[-2]
        if len(sld) >= 8 and entropy(sld) > 3.5:
            print(f'{entropy(sld):.2f}\t{name}')
" | sort -rn | head -30

echo ""
echo "=== 6. Files with known-bad hashes (from files.log) ==="
KNOWN_BAD_MD5="d41d8cd98f00b204e9800998ecf8427e"  # Example (empty file MD5)
awk -v bad="$KNOWN_BAD_MD5" 'NR>8 && !/^#/ && $NF == bad {print}' \
  "$LOG_DIR/files.log" 2>/dev/null | head -10

echo ""
echo "=== 7. SSH login attempts (many SYN to port 22) ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/conn.log" | \
  awk -F'\t' '$6 == "22" && $8 ~ /S0|REJ/ {print $3"\t"$5}' | \
  sort | uniq -c | sort -rn | head -10
Why the uid field is Zeek's most important correlation key

Every Zeek log entry related to a single network connection shares the same uid value. A TCP connection to an HTTPS server generates entries in conn.log, ssl.log, files.log (if files were transferred), and possibly http.log (if the TLS is decrypted). All of these entries have the same uid. This lets you start with one log, find a suspicious uid, then retrieve all related records: grep uid_value conn.log ssl.log files.log x509.log — instant multi-protocol view of one connection. In SIEM queries: when a Suricata alert fires with a connection uid, look up that same uid in Zeek's ssl.log to get the JA3 hash and certificate, in conn.log for the data volume, and in dns.log for the DNS name that resolved to the destination IP (look for dns log entries near the connection timestamp from the same source). The uid is the glue that makes correlated multi-log analysis practical.

Q & A

Q: conn.log shows duration="-" for many connections. What does that mean and should I be concerned?

Duration "-" in Zeek's conn.log means the connection was still open when Zeek generated the log entry (typically at log rotation time or capture end). Zeek writes a conn.log entry when a connection closes (FIN/RST) or when it times out. For connections that are still active at log rotation time, Zeek writes a partial entry with duration="-". This is normal for: long-lived TLS sessions, persistent TCP connections (keepalive), WebSocket connections, and SSH/RDP sessions. It becomes significant if you see many connections with duration="-" that should have completed: this may indicate the connection is a long-running tunnel (C2 over TLS, SSH tunnel), or that you have packet loss causing connections to appear open when they've actually closed. For hunting: search for connections with duration="-" that have large orig_bytes or resp_bytes — these are long-running connections that transferred significant data and never closed during the observation window. A C2 beacon that maintains a persistent connection to avoid SYN/SYN-ACK noise would show exactly this pattern.