DNS EXFIL
LABEL BYTES
PER ZONE
ENCODING
DetectionBlue TeamSOCOctober 12, 2026•9 min read

Detecting DNS Exfiltration: Bulk Data Over Queries

DNS C2 asks and answers. DNS exfiltration mostly asks. The data is in the question, and the volume per zone gives it away.

Scroll

This page is the exfiltration companion to DNS C2 detection. A C2 channel is a conversation; an exfil channel is a one-way upload encoded into subdomain labels. That changes what you measure: instead of check-in cadence you estimate how many payload bytes a host has pushed into one parent zone, and compare that with how much the zone could plausibly need.

Signals at a Glance

SignalWhereATT&CK
Estimated encoded bytes per (host, parent zone) per hourResolver, Zeek dns.logT1048.001
Long labels with high character entropy and few repeatsDNS logsT1048.001
Sequence or counter prefixes in labels (chunk numbering)DNS logsT1048.001
Few or uniform answers to a very large number of unique namesDNS logsT1572
One workstation producing a large share of a zone’s total queriesDNS logsT1048

Estimating Payload per Zone

Base32 packs 5 bits per character, so a label of 50 characters carries roughly 30 bytes of payload. Summing label characters per host and zone gives a conservative upper bound on how much data left through DNS. Rank by that estimate, not by raw query count:

index=zeek sourcetype="bro:dns:json" qtype_name IN ("A","AAAA","TXT","CNAME","MX")
| rename "id.orig_h" as src
| eval parts = split(query, ".")
| eval n = mvcount(parts)
| eval zone = mvindex(parts, n-2) . "." . mvindex(parts, n-1)
| eval sub = mvjoin(mvindex(parts, 0, n-3), "")
| eval sublen = len(sub)
| bin _time span=1h
| stats count as queries sum(sublen) as label_chars dc(query) as unique_names avg(sublen) as avg_label by _time, src, zone
| eval est_kb = round(label_chars * 0.625 / 1024, 1)
| where est_kb > 256 AND unique_names > 500 AND avg_label > 20
| sort - est_kb
``` tune/validate: exclude CDN, AV and mail-filtering zones using a lookup ```

Pair the estimate with a randomness check. A host name such as a printer or a cloud-sync endpoint repeats; encoded data does not. The ratio of unique names to total queries approaches one for exfil:

// Windows DNS / Sentinel (KQL) - near-unique, long-label queries per zone
// tune/validate: Name and ClientIP fields assume the DnsEvents table
DnsEvents
| where TimeGenerated > ago(1d)
| extend labels = split(Name, ".")
| extend zone = strcat(labels[array_length(labels)-2], ".", labels[array_length(labels)-1])
| extend sub = tostring(labels[0])
| summarize q = count(), uniq = dcount(Name), avgSub = avg(strlen(sub)),
            maxSub = max(strlen(sub)) by ClientIP, zone, bin(TimeGenerated, 1h)
| extend uniqRatio = todouble(uniq) / q
| where q > 300 and uniqRatio > 0.9 and avgSub > 24
| order by q desc

Separating Exfil from Telemetry

Vetted sources

Zeek dns.log with RITA, SigmaHQ DNS rules, Elastic detections for DNS tunnelling, and MITRE CAR analytics on DNS volume. Baseline your own environment — security vendors and CDNs are the main false-positive source, and the payload estimate above is an upper bound that should be validated against a decode.

Build Detections That Fire

Mjolnir Security helps blue teams turn threat intelligence into high-fidelity detections across endpoint, network, identity and cloud telemetry.

Detection EngineeringManaged Detection & ResponseThreat HuntingIncident ResponsePurple Teaming
  • Detection Engineering We build and tune detections mapped to ATT&CK from your own telemetry.
  • Threat Hunting as a Service Hypothesis-driven hunts for the behaviours on this page.
  • 24/7 Incident Response Containment when a detection fires. Call +1 833 403 5875.
Written by: Mjolnir Threat Research (MTAC)  |  Published: October 12, 2026  |  Last reviewed: October 12, 2026