DNS Tunneling Walkthrough
This walkthrough covers the forensic analysis of a confirmed dnscat2 DNS tunneling session: identifying it from DNS query patterns, reconstructing the tunnel session, and determining what data traversed it. DNS tunneling is used both for C2 (in environments where HTTPS is blocked) and for data exfiltration (in environments with strict HTTP DLP but permissive DNS).
A locked-down environment blocks all outbound HTTP/HTTPS from server VLANs, but allows outbound DNS to the corporate resolver, which forwards to the internet. An attacker with foothold on a server in this VLAN uses dnscat2 to establish a bidirectional covert channel via DNS to their attacker-controlled authoritative name server. The attack is invisible at the perimeter — no TCP/TLS connections — but leaves unmistakable patterns in the DNS log.
dnscat2 Traffic Analysis
dnscat2 DNS Tunnel — Network Signatures
═══════════════════════════════════════════════════════════════════
Normal DNS traffic:
├── Query volume: 2-20 queries per minute (web browsing, email)
├── Query types: A (90%), AAAA (8%), CNAME, MX, TXT (rare)
├── Query names: human-readable domain names
├── Label length: typically 5-20 characters per label
└── Per-SLD query volume: rarely >100 queries in an hour
dnscat2 tunnel traffic:
├── Query volume: hundreds to thousands per minute
├── Query types: TXT and CNAME preferred (larger response capacity)
├── Query names: subdomains encoded in base32 or hex:
│ 07c65b2e00010000048576c8.attacker.c2
│ Session ID (4 bytes hex) + sequence + payload
├── Label length: 40-60+ characters
└── All queries share the same SLD (attacker's domain)
dnscat2 PCAP signatures:
└── All queries: [session_id][seq][cmd_type][payload_hex].attacker.c2
Example: 07c65b2e000100000000006e6f74.attacker.c2 (TXT query)
First 8 chars: session ID (changes per session)
Chars 9-16: sequence + flags
Remaining: base32/hex encoded payload
DNS Tunnel Investigation
#!/bin/bash
LOG_DIR="${1:-/opt/zeek/logs/current}"
PCAP="${2:-capture.pcap}"
echo "=== Step 1: Identify high-volume DNS to single SLD ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/dns.log" | \
awk -F'\t' '{
domain = $9
n = split(domain, parts, ".")
if (n >= 2) sld = parts[n-1] "." parts[n]
else sld = domain
count[sld"→"$3]++
}
END {
for (k in count)
if (count[k] > 200) print count[k], k
}' | sort -rn | head -20
echo ""
echo "=== Step 2: Check for long subdomain labels ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/dns.log" | \
awk -F'\t' '{
split($9, labels, ".")
for (i in labels)
if (length(labels[i]) > 40)
print $1"\t"$3"\t"$9"\t"length(labels[i])
}' | head -20
echo ""
echo "=== Step 3: Entropy analysis of subdomain labels ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/dns.log" | \
awk -F'\t' '{print $9}' | sort -u | python3 -c "
import sys, math
from collections import Counter
def entropy(s):
c = Counter(s)
t = len(s)
return -sum((v/t)*math.log2(v/t) for v in c.values()) if t else 0
for line in sys.stdin:
domain = line.strip()
if '.' not in domain: continue
label = domain.split('.')[0]
if len(label) >= 20:
ent = entropy(label)
if ent > 3.8:
print(f'{ent:.2f}\t{domain}')
" | sort -rn | head -30
echo ""
echo "=== Step 4: Identify attacker's SLD from anomalous queries ==="
# The tunnel domain is the SLD that appears in high-entropy queries
echo "The SLD appearing most frequently in long/high-entropy subdomain queries:"
awk 'NR>8 && !/^#/' "$LOG_DIR/dns.log" | \
awk -F'\t' '{
split($9, parts, ".")
n = length(parts)
if (n >= 2 && length(parts[1]) >= 40) {
sld = parts[n-1] "." parts[n]
count[sld]++
}
}
END {for(s in count) print count[s], s}' | sort -rn | head -5
echo ""
echo "=== Step 5: dnscat2 session reconstruction ==="
TUNNEL_DOMAIN="${3:-attacker.c2}"
tshark -r "$PCAP" -Y "dns.qry.type==16 && dns.qry.name contains \".$TUNNEL_DOMAIN\"" \
-T fields -e frame.time_epoch -e ip.src -e dns.qry.name 2>/dev/null | \
python3 -c "
import sys
import re
sessions = {}
# dnscat2 format: [4-byte-hex-session-id][other][payload].[domain]
for line in sys.stdin:
parts = line.strip().split('\t')
if len(parts) < 3: continue
ts, src, name = parts
label = name.split('.')[0]
if len(label) >= 8:
session_id = label[:8]
if session_id not in sessions:
sessions[session_id] = {'count': 0, 'first': ts, 'last': ts, 'src': src}
sessions[session_id]['count'] += 1
sessions[session_id]['last'] = ts
print('\nReconstructed dnscat2 sessions:')
for sid, info in sorted(sessions.items(), key=lambda x: -x[1]['count']):
if info['count'] > 5:
duration = float(info['last']) - float(info['first'])
print(f' Session {sid}: {info[\"count\"]} packets over {duration:.0f}s from {info[\"src\"]}')
"
echo ""
echo "=== Step 6: Payload volume estimate ==="
echo "Estimating data volume in tunnel:"
tshark -r "$PCAP" -Y "dns.qry.type==16 && dns.qry.name contains \".$TUNNEL_DOMAIN\"" \
-T fields -e dns.qry.name 2>/dev/null | \
awk '{
split($0, parts, ".")
label = parts[1]
if (length(label) > 16)
total += (length(label) - 16) * 0.75 # ~75% base32 efficiency
}
END {printf "Estimated payload: %.1f KB\n", total/1024}'
Lab examples of DNS tunneling use iodine or dnscat2 with obvious patterns: thousands of queries per minute to a clearly attacker-controlled domain. In practice, sophisticated DNS tunneling is slower and more subtle: (1) Legitimate CDNs provide cover: cloud services like Akamai, Cloudflare, and Fastly use long, random-looking subdomains for content delivery. Subdomain label length alone is insufficient as a detector — you need to combine it with per-SLD query volume and entropy that's higher than CDN patterns. (2) Low-and-slow tunneling: an attacker who exfiltrates 1 MB/day via DNS (1-2 queries per minute to a single domain) is below every threshold-based detector. Only 30-day volume analysis of per-SLD query totals catches this. (3) CNAME/A record tunneling: instead of TXT records (which have large capacity but are unusual), some tunnels use CNAME or A record queries, encoding data as hex in the subdomain label. These are more common in DNS query logs and require entropy-based detection rather than type-based. The key insight: DNS tunneling is fundamentally a per-SLD query volume problem. Any SLD that receives more queries per day than can be explained by normal web activity for that domain warrants investigation, regardless of the specific encoding used.
Q & A
Q: I confirmed DNS tunneling, but the DNS logs only show the queries going out. How do I know what data was in the tunnel?
Recovering the content of DNS tunnel payloads from logs alone is generally not possible — you need PCAP that includes the DNS responses. The data is encoded in: (1) Subdomains of queries (client → server direction, visible in query logs), (2) Response payloads like TXT records, CNAME target names, A records returning encoded data (server → client direction, visible only in PCAP responses). If you only have query logs: you can determine the volume and session structure, but not the decrypted content. If you have PCAP with both query and response packets: you can reconstruct the dnscat2 stream by reassembling the sequence of payloads, then decode them. dnscat2's protocol is documented; the reassembled stream is typically encrypted with a session key negotiated at the start of the tunnel. Without the key (not in the PCAP), the reconstructed stream is ciphertext. Forensic value of partial reconstruction: even without decryption, you can determine the total data volume, the session duration, and the timing of data transfers. Combined with timeline context (what was the attacker doing at the server during the tunnel session?), this narrows down what was likely exfiltrated.