← Blog
// BLUE TEAM Β· THREAT HUNTING

Detecting DNS Tunneling with KQL in Microsoft Sentinel

Published Jul 14, 2026 Β· 8 min read
KQLMicrosoft SentinelDNSThreat HuntingExfiltraciΓ³n

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?

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
πŸ’‘ Analyst tip: combine the three signals into one Sentinel analytics rule and score each host. A single signal is noisy; all three together rarely fail. Add a watchlist of known corporate domains to exclude false positives (EDR telemetry, CDNs, etc.).

From detection to response

  1. Triage: is the parent domain known? Is the process issuing the queries legitimate (correlate with Defender DeviceNetworkEvents)?
  2. Containment: block the domain at the resolver/EDR and isolate the host if confirmed.
  3. Eradication: identify the responsible binary and its persistence.
  4. 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.

Sergio Belmonte Morales
Sergio Belmonte Morales
Cybersecurity Analyst Β· SOC Β· Sentinel/KQL specialist