Network triage with flows and DNS
Read NetFlow and Zeek conn and dns logs to answer who talked to whom, how much and how regularly. Spot beaconing, DNS tunnelling and egress anomalies, and know when a pcap is worth asking for and what it adds.
01Flows: who talked to whom
- Read NetFlow and Zeek conn.log fields fluently
- Recognise beaconing from timing and size regularity
- Spot DNS tunnelling and odd egress in dns.log and flow data
- Know what a pcap adds and when it is worth asking for
Flow records are the network's call log: no content, just who, where, when, how long and how much. They are cheap to keep for months, which makes them the best source for "did anything else talk to this IP?" long after packets are gone.
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state
1763024401.112 CkX9a1 10.20.4.17 51122 185.220.101.4 443 tcp ssl 0.41 512 1840 SF
1763024461.098 Cj2Lq8 10.20.4.17 51130 185.220.101.4 443 tcp ssl 0.39 512 1840 SF
1763024521.140 CbT7m3 10.20.4.17 51141 185.220.101.4 443 tcp ssl 0.42 512 1840 SF
1763024533.502 CqP0z6 10.20.4.17 51149 10.20.0.5 53 udp dns 0.01 44 120 SF
1763024581.087 CwE4h2 10.20.4.17 51150 185.220.101.4 443 tcp ssl 0.40 512 1840 SF| Field | Meaning | Analyst use |
|---|---|---|
| id.orig_h / id.resp_h | Initiator and responder | Direction: who started it matters more than who sent more |
| duration | Connection length in seconds | Long-lived sessions (tunnels) vs short repeated ones (beacons) |
| orig_bytes / resp_bytes | Payload each way | Uploads where orig_bytes dwarfs resp_bytes: exfil candidates |
| conn_state | How it ended (SF normal, S0 no reply, REJ refused) | Lots of S0/REJ: scanning or dead C2 |
| uid | Zeek connection ID | Joins conn to dns, ssl, http and files logs for the same session |
Read the sample above: every 60 seconds, the same host opens a short TLS connection to the same external IP, sends exactly 512 bytes and gets exactly 1840 back. People do not browse like that. Software on a timer does.
02Beaconing
A beacon is implant software checking in with its C2 server: "any tasks for me?". Most frameworks add jitter (random variation in the interval) to hide, but the pattern survives statistics: many connections, regular gaps, similar sizes, to a destination few other hosts talk to.
// Hosts with regular, small, repeated outbound connections (Zeek conn in Sentinel)
Zeek_Conn
| where TimeGenerated > ago(24h) and not(ipv4_is_private(id_resp_h))
| sort by id_orig_h, id_resp_h, TimeGenerated asc
| extend gap = datetime_diff('second', TimeGenerated, prev(TimeGenerated))
| where id_orig_h == prev(id_orig_h) and id_resp_h == prev(id_resp_h)
| summarize conns=count(), avg_gap=avg(gap), sd_gap=stdev(gap), sizes=dcount(orig_bytes) by id_orig_h, id_resp_h
| where conns > 50 and sd_gap < avg_gap * 0.2 and sizes < 5
| order by conns desc
// id_orig_h id_resp_h conns avg_gap sd_gap sizes
// 10.20.4.17 185.220.101.4 1438 60.1 3.8 1
// 10.20.9.30 52.168.112.66 2880 30.0 0.2 2 <- Defender telemetry endpoint| Signal | Beacon-like | Usually benign |
|---|---|---|
| Interval | Fixed or jittered around a value (60 s ± 10%) | Irregular, clustered in working hours |
| Destination prevalence | One or two internal hosts talk to it | Hundreds of hosts (update, AV, SaaS) |
| Size | Near-identical request and response sizes | Varied sizes |
| Time of day | Continues at 03:00 with nobody logged on | Follows user activity |
| Destination age | Domain registered last week, cheap TLD, no reputation | Long-lived vendor domain |
EDR agents, AV updates, chat clients and telemetry all check in on timers. Combine regularity with rarity: a beacon to an endpoint 4,000 hosts also contact is almost always your own tooling. The second result above is Defender, which is why prevalence is the next filter.
Which pair of signals most strongly suggests C2 beaconing rather than legitimate telemetry?
03DNS: tunnelling and odd resolution
DNS is allowed out of almost every network, so attackers use it as a transport: data is encoded into subdomain labels on queries, and replies carry commands in TXT, NULL or CNAME records. The traffic looks nothing like a person browsing.
#fields ts id.orig_h query qtype_name rcode_name answers
1763031002.21 10.20.4.17 a3f9c2e1b07d4e55a1c9.0e7b2d4f61a8c3e9b1d2.x1.cdn-sync-api.top TXT NOERROR "dGFzazpzbGVlcCA2MA=="
1763031002.84 10.20.4.17 b81d0c77e2a945f0be13.94c6a1e0f3b72d58c4a6.x2.cdn-sync-api.top TXT NOERROR "b2s="
1763031003.40 10.20.4.17 5c2e9a1f04b8d3e7c6a2.f1b0e9d84c27a3b5e061.x3.cdn-sync-api.top TXT NOERROR "b2s="
1763031004.07 10.20.4.17 e7d14b2a9c3f0815d6e4.2a9e7c1b5f3d08e4a7c6.x4.cdn-sync-api.top TXT NOERROR "b2s="
1763031010.55 10.20.6.12 outlook.office365.com A NOERROR 52.97.146.178| Tunnelling sign | Why it stands out |
|---|---|
| Long, high-entropy labels (hex or base32) | Humans and CDNs do not query 40 random characters |
| Many unique subdomains under one parent domain | Each query carries a new chunk of data, so nothing repeats |
| TXT, NULL or unusual qtypes at volume | Normal clients mostly ask for A, AAAA and HTTPS records |
| One internal host, huge query count to one domain | Prevalence and volume both wrong |
| Queries bypassing the corporate resolver | Direct port 53 or DoH to the internet avoids your logging |
# Unique subdomains per parent domain, top talkers (zeek-cut on dns.log)
zcat dns.*.log.gz | zeek-cut id.orig_h query \
| awk '{n=split($2,a,"."); if(n>=2) print $1, a[n-1]"."a[n], $2}' \
| sort -u | awk '{print $1, $2}' | sort | uniq -c | sort -rn | head
# 9412 10.20.4.17 cdn-sync-api.top
# 311 10.20.6.12 office365.comThe TXT answers above are base64: dGFzazpzbGVlcCA2MA== decodes to task:sleep 60. You rarely get it that easy, but decoding a few answers often turns "suspicious" into "confirmed C2" in one step.
04Egress anomalies and what a pcap adds
Exfiltration shows up in flows as direction and volume: an internal host sending far more than it receives, to a destination it has never used, often outside working hours. Compare against that host's own history, not a global average.
src dst dport start(UTC) dur bytes_out bytes_in first_seen(dst)
10.30.2.44 45.137.21.9 443 02:11 47m 18.6 GB 41 MB today
10.30.2.44 10.30.0.10 445 01:05-02:09 64m 12 MB 17.9 GB - (file server)Read it as a sequence: between 01:05 and 02:09 the host pulled almost 18 GB from the file server over SMB, then at 02:11 pushed 18.6 GB to an external IP it had never contacted. That is collection then exfiltration, and the flow data alone is enough to escalate.
| Source | Gives you | Does not give you |
|---|---|---|
| NetFlow / IPFIX | 5-tuple, bytes, packets, times; long retention | Names, content, TLS details |
| Zeek conn/dns/ssl/http | Flows plus DNS names, SNI, certificates, HTTP metadata, file hashes | Payload of encrypted sessions |
| Full pcap | Every byte: payloads in clear protocols, exact handshakes, malformed traffic | Long retention (days at best); decrypted TLS without keys |
"I start with flows to scope who talked to the destination and when, use Zeek DNS and SSL logs to put names and certificates on it, and only ask for packets when the protocol is clear-text or abused, because a pcap of TLS mostly tells me what the flow already did."
Q1How do you catch DNS over HTTPS bypassing your resolver?+
Block or alert on known DoH endpoints and on direct outbound 53/853 from anything that is not a resolver; then dns.log stays complete. Browsers can be told to use enterprise DNS by policy.
Q2Why do flows matter more than pcap for most investigations?+
Retention. Flows are kept for months; packets for hours or days. Most investigations start days after the intrusion, when only flows remain.
Q3What is JA3 or JA4?+
A fingerprint of the TLS client hello. The same malware family tends to produce the same fingerprint regardless of destination, so it lets you pivot across IPs and domains without decrypting anything.
A host shows 18 GB inbound from a file server over SMB, then 18 GB outbound to a new external IP. What do you do next?