Chapter 43

DNS over HTTPS C2

DoH-based C2 combines the invisibility of DNS tunneling with the plausibility of HTTPS traffic. An attacker who controls their own DoH resolver (an HTTPS server implementing RFC 8484) can encode C2 commands in DNS responses delivered over HTTPS. From the network's perspective, the traffic looks identical to legitimate web browsing to port 443 — no distinct DNS packets on UDP/53, no suspicious subdomain labels visible in plaintext. Detection requires identifying DoH sessions specifically rather than treating all HTTPS as opaque.

Scenario

Malware connects to what appears to be a web server on port 443. The SNI is a benign-looking domain. In PCAP you see: small HTTP/2 POST requests every 30 seconds, each with Content-Type: application/dns-message. This is the RFC 8484 DoH content type — the malware is making DNS queries over HTTPS to an attacker-controlled DoH server that returns encoded C2 commands in DNS response records. The network sees HTTPS traffic; the analyst who knows the DoH content type immediately identifies it as DoH-based C2.

DoH Protocol Forensics

  DNS over HTTPS (RFC 8484) — Wire Format
  ═══════════════════════════════════════════════════════════════════

  DoH client → DoH server: HTTPS POST or GET

  POST format (most common for C2 use):
    POST /dns-query
    Content-Type: application/dns-message
    Accept: application/dns-message
    [binary DNS message in body]

  GET format:
    GET /dns-query?dns=BASE64URL_ENCODED_DNS_MESSAGE
    Accept: application/dns-message

  Server response:
    Content-Type: application/dns-message
    [binary DNS response in body]

  Key forensic signal: Content-Type = "application/dns-message"
  This MIME type is specific to DoH — no legitimate web content
  uses this type. In decrypted TLS traffic, its presence identifies DoH.

  C2 encoding in DoH:
  ├── Command encoding: attacker encodes command in DNS TXT record value
  │     Client queries ENCODEDCMD.c2.attacker.com via DoH
  │     Server returns TXT record with encoded response
  └── Exfil encoding: same as DNS tunneling but over HTTPS

  Legitimate DoH servers: 1.1.1.1, 8.8.8.8, 9.9.9.9, 149.112.112.112
  C2 DoH server: any attacker-controlled HTTPS server
  Detection: DoH Content-Type to unknown IPs = suspicious

DoH C2 Detection

bashdoh-c2-detect.sh
#!/bin/bash
PCAP="$1"
KEYLOG="${2:-}"  # Optional SSLKEYLOGFILE for decrypted analysis

# Without decryption: identify DoH sessions by destination
echo "=== Known DoH resolver connections ==="
# Known public DoH resolvers
DOH_RESOLVERS="1.1.1.1|1.0.0.1|8.8.8.8|8.8.4.4|9.9.9.9|149.112.112.112"
tshark -r "$PCAP" -n \
  -Y "tcp.dstport==443 and tls.handshake.type==1" \
  -T fields \
  -E separator="\t" \
  -e ip.src -e ip.dst \
  -e tls.handshake.extensions_server_name \
  | grep -E "(cloudflare-dns|dns\.google|dns\.quad9|doh)" \
  | sort | uniq -c | sort -rn | head -20

echo ""
echo "=== HTTPS to IPs with DoH-related DNS names ==="
tshark -r "$PCAP" -n \
  -Y "tcp.dstport==443 and tls.handshake.extensions_server_name matches \"dns|doh|resolver\"" \
  -T fields \
  -E separator="\t" \
  -e ip.src -e ip.dst -e tls.handshake.extensions_server_name \
  | sort | uniq -c | sort -rn | head -20

# With decryption: identify DoH content type
if [ -n "$KEYLOG" ]; then
    echo ""
    echo "=== DoH POST requests (Content-Type: application/dns-message) ==="
    tshark -r "$PCAP" \
      -o "tls.keylog_file:${KEYLOG}" \
      -Y "http.content_type == \"application/dns-message\"" \
      -T fields \
      -E separator="\t" \
      -e frame.time_epoch -e ip.src -e ip.dst \
      -e http.request.method -e http.host -e http.request.uri \
      | head -30

    echo ""
    echo "=== DoH responses to decode ==="
    tshark -r "$PCAP" \
      -o "tls.keylog_file:${KEYLOG}" \
      -Y "http2.headers.content-type == \"application/dns-message\" and http2.type==0" \
      -T fields \
      -E separator="\t" \
      -e frame.time_epoch -e ip.src -e ip.dst \
      | head -20
fi

echo ""
echo "=== Beaconing to DoH-like endpoints (30-300s regular intervals) ==="
tshark -r "$PCAP" -n \
  -Y "tcp.dstport==443 and tls.handshake.type==1" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch -e ip.src -e ip.dst \
  -e tls.handshake.extensions_server_name \
  | grep -v "$(echo "$DOH_RESOLVERS" | tr '|' '\n' | head -1)" \
  | python3 -c "
import sys
from collections import defaultdict
import math

timestamps = defaultdict(list)
for line in sys.stdin:
    parts = line.strip().split('\t')
    if len(parts) >= 3:
        try:
            ts, src, dst = float(parts[0]), parts[1], parts[2]
            sni = parts[3] if len(parts) > 3 else ''
            timestamps[(src, dst, sni)].append(ts)
        except:
            pass

for key, ts_list in timestamps.items():
    ts_list.sort()
    if len(ts_list) < 10:
        continue
    intervals = [ts_list[i+1]-ts_list[i] for i in range(len(ts_list)-1)]
    intervals = [iv for iv in intervals if 0 < iv < 7200]
    if len(intervals) < 5:
        continue
    mean = sum(intervals)/len(intervals)
    if mean < 1:
        continue
    variance = sum((x-mean)**2 for x in intervals)/len(intervals)
    cv = math.sqrt(variance)/mean
    if cv < 0.2 and 10 <= mean <= 600:
        src, dst, sni = key
        print(f'BEACON CV={cv:.3f} interval={mean:.0f}s n={len(ts_list)} {src}→{dst} ({sni})')
" | sort -t= -k2 -n | head -20
Common mistake: blocking all DoH without visibility

A common defensive response to DoH C2 risk is to block all HTTPS to known DoH resolver IPs (1.1.1.1, 8.8.8.8, etc.) at the firewall. This prevents legitimate clients from using public DoH resolvers — but it does nothing against an attacker using a custom DoH server on their own domain. An attacker's C2 DoH server is at an attacker-controlled IP, not at Cloudflare's 1.1.1.1. Blocking known DoH resolvers hurts legitimate users without meaningfully reducing attack surface. The more effective approach: (1) Monitor for DoH MIME types in decrypted HTTPS traffic (requires TLS inspection or SSLKEYLOGFILE in lab), (2) Use DNS Security (DNSSEC + response policy zones) on your corporate DNS resolvers and block DoH from reaching external resolvers — force all DNS through your monitored resolver, (3) Identify DoH sessions through behavioral analysis (beaconing to 443 + no SNI match to known CDN/SaaS + small consistent request size) rather than blocking specific IPs.

Q & A

Q: Is there a way to identify DoH traffic without decrypting TLS?

Without decryption, DoH is nearly indistinguishable from normal HTTPS. However, several heuristics help: (1) SNI matching known DoH providers: if the SNI is cloudflare-dns.com, dns.google, or doh.opendns.com, it's almost certainly DoH. These are the most common public DoH endpoints and are identifiable from the ClientHello alone. (2) Request size patterns: DoH POST requests carry a binary-encoded DNS message, typically 20–100 bytes. This produces a distinctive pattern of small, consistent request sizes to the same endpoint. Normal HTTPS to the same CDN would have more varied request sizes. (3) Response size patterns: DoH responses carry binary DNS response messages (typically 50–500 bytes for a simple A/TXT query). A server that always responds with 50–200 byte responses to all requests looks different from a web server serving content. (4) ALPN: DoH uses HTTP/2. If the TLS ClientHello ALPN includes h2 and the destination is a known DoH IP, the combination is informative. (5) Custom DoH C2: if the attacker is using their own DoH server (not a known public resolver), the destination IP or SNI will not match any known pattern — identification falls back to behavioral analysis (beaconing + small consistent requests).