Detecting DNS Tunneling with KQL in Microsoft Sentinel
DNS is the "phone book" of the internet, and almost no firewall blocks it. That is exactly why attackers abuse it: they encapsulate data inside DNS queries and responses to exfiltrate information or keep a Command & Control (C2) channel that flies under the radar. Let's hunt it in Microsoft Sentinel with KQL, no extra tooling required.
Why is DNS an ideal covert channel?
- It is rarely inspected at the content level and usually allowed outbound.
- Every queried subdomain can carry encoded data (Base32/Base64/hex).
- Tools like
iodine,dnscat2orDNSExfiltratorautomate it.
The footprint is characteristic: long, high-entropy subdomains, an anomalous volume of queries to a single domain, and unusual record types (TXT, NULL, CNAME).
Signal 1 β Subdomain length and entropy
A legit domain like cdn.contoso.com is short and pronounceable. A tunnel produces something like MFRGGZDF.k7x2.exfil.attacker.com. We can approximate entropy by comparing unique characters to length:
DnsEvents
| where TimeGenerated > ago(24h)
| where isnotempty(Name)
| extend sub = tostring(split(Name, ".")[0])
| extend subLen = strlen(sub)
| extend uniqueChars = array_length(set_difference(
extract_all(@"(.)", sub), dynamic([])))
| where subLen > 30 and uniqueChars > 15
| project TimeGenerated, ClientIP, Name, subLen, uniqueChars
| order by subLen desc
Tune the thresholds (subLen, uniqueChars) to your environment after a baseline phase.
Signal 2 β Anomalous volume per parent domain
A host making hundreds of unique queries to the same parent domain in a short window is suspicious: it is "leaking" data subdomain by subdomain.
DnsEvents
| where TimeGenerated > ago(1h)
| extend parts = split(Name, ".")
| extend parentDomain = strcat(parts[array_length(parts)-2], ".", parts[array_length(parts)-1])
| summarize queries = count(), uniqueSub = dcount(Name) by ClientIP, parentDomain
| where uniqueSub > 200
| order by uniqueSub desc
Signal 3 β Uncommon record types
Normal traffic is mostly A/AAAA. A spike of TXT or NULL from a user endpoint is a red flag:
DnsEvents
| where TimeGenerated > ago(24h)
| where QueryType in ("TXT", "NULL", "CNAME")
| summarize count() by ClientIP, QueryType, bin(TimeGenerated, 1h)
| where count_ > 50
From detection to response
- Triage: is the parent domain known? Is the process issuing the queries legitimate (correlate with Defender
DeviceNetworkEvents)? - Containment: block the domain at the resolver/EDR and isolate the host if confirmed.
- Eradication: identify the responsible binary and its persistence.
- Lessons learned: turn the hunt into a scheduled analytics rule so you never repeat the manual work.
If you want to practice the offensive/forensic side, the platform has the OP: GHOST_TRAFFIC challenge, where you analyze a capture with a real DNS tunnel.