]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add a common base for CVE-2025-40778 tests
authorPetr Špaček <pspacek@isc.org>
Fri, 11 Jul 2025 16:37:57 +0000 (18:37 +0200)
committerMichał Kępień <michal@isc.org>
Mon, 22 Dec 2025 10:58:39 +0000 (11:58 +0100)
Add the zone files, configuration, and code that will be reused by all
tests related to CVE-2025-40778.

Co-authored-by: Michał Kępień <michal@isc.org>
.gitlab-ci.yml
bin/tests/system/bailiwick/ans1/ans.py [new file with mode: 0644]
bin/tests/system/bailiwick/ans1/root.db [new file with mode: 0644]
bin/tests/system/bailiwick/ans2/ans.py [new file with mode: 0644]
bin/tests/system/bailiwick/ans2/victim.db [new file with mode: 0644]
bin/tests/system/bailiwick/ans3/ans.py [new symlink]
bin/tests/system/bailiwick/ans3/attacker.db [new file with mode: 0644]
bin/tests/system/bailiwick/ans3/victim.db [new file with mode: 0644]
bin/tests/system/bailiwick/bailiwick_ans.py [new file with mode: 0644]
bin/tests/system/bailiwick/ns4/named.conf.j2 [new file with mode: 0644]
bin/tests/system/bailiwick/tests_bailiwick.py [new file with mode: 0644]

index dfb583ce5cbf21b46ff444404d2e0740e2916fc1..243a98c88fda58ac6d843b43459d53850601ab8c 100644 (file)
@@ -679,7 +679,7 @@ vulture:
   <<: *python_triggering_rules
   needs: []
   script:
-    - vulture --exclude "*ans.py,conftest.py,re_compile_checker.py,isctest" --ignore-names "after_servers_start,bootstrap,pytestmark" bin/tests/system/
+    - vulture --exclude "*ans.py,conftest.py,re_compile_checker.py,isctest" --ignore-names "after_servers_start,bootstrap,pytestmark,autouse_*" bin/tests/system/
 
 ci-variables:
   <<: *precheck_job
diff --git a/bin/tests/system/bailiwick/ans1/ans.py b/bin/tests/system/bailiwick/ans1/ans.py
new file mode 100644 (file)
index 0000000..be072a3
--- /dev/null
@@ -0,0 +1,37 @@
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0.  If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+from typing import AsyncGenerator
+
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import (
+    DnsResponseSend,
+    QueryContext,
+    ResponseAction,
+)
+
+from bailiwick_ans import ResponseSpoofer, spoofing_server
+
+
+ATTACKER_IP = "10.53.0.3"
+TTL = 3600
+
+
+def main() -> None:
+    spoofing_server().run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/bailiwick/ans1/root.db b/bin/tests/system/bailiwick/ans1/root.db
new file mode 100644 (file)
index 0000000..6469372
--- /dev/null
@@ -0,0 +1,24 @@
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0.  If a copy of the MPL was not distributed with this
+; file, you can obtain one at https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+$ORIGIN .
+$TTL 3600
+.                      SOA     . . 0 0 0 0 3600
+.                      NS      a.root-servers.nil.
+a.root-servers.nil.    A       10.53.0.1
+
+; queries should go here ... unless the attack succeeded
+victim.                        NS      ns.victim.
+ns.victim.             A       10.53.0.2
+
+; no query should go here
+attacker.              NS      ns.attacker.
+ns.attacker.           A       10.53.0.3
diff --git a/bin/tests/system/bailiwick/ans2/ans.py b/bin/tests/system/bailiwick/ans2/ans.py
new file mode 100644 (file)
index 0000000..be072a3
--- /dev/null
@@ -0,0 +1,37 @@
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0.  If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+from typing import AsyncGenerator
+
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import (
+    DnsResponseSend,
+    QueryContext,
+    ResponseAction,
+)
+
+from bailiwick_ans import ResponseSpoofer, spoofing_server
+
+
+ATTACKER_IP = "10.53.0.3"
+TTL = 3600
+
+
+def main() -> None:
+    spoofing_server().run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/bailiwick/ans2/victim.db b/bin/tests/system/bailiwick/ans2/victim.db
new file mode 100644 (file)
index 0000000..d5b4d25
--- /dev/null
@@ -0,0 +1,17 @@
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0.  If a copy of the MPL was not distributed with this
+; file, you can obtain one at https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+$TTL 3600
+@                      SOA     ns.victim. . 0 0 0 0 3600
+@                      NS      ns
+ns                     A       10.53.0.2
+prime                  TXT     "this record is used for priming the cache of the targeted resolver"
+canary                 TXT     "correct answer from the domain under attack"
diff --git a/bin/tests/system/bailiwick/ans3/ans.py b/bin/tests/system/bailiwick/ans3/ans.py
new file mode 120000 (symlink)
index 0000000..2416f57
--- /dev/null
@@ -0,0 +1 @@
+../../ans.py
\ No newline at end of file
diff --git a/bin/tests/system/bailiwick/ans3/attacker.db b/bin/tests/system/bailiwick/ans3/attacker.db
new file mode 100644 (file)
index 0000000..e7474bb
--- /dev/null
@@ -0,0 +1,17 @@
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0.  If a copy of the MPL was not distributed with this
+; file, you can obtain one at https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+$TTL 3600
+@                      SOA     ns.attacker. . 0 0 0 0 3600
+@                      NS      ns
+ns                     A       10.53.0.3
+only-if-hijacked       TXT     "this record only exists in the hijacked version of the zone"
+canary                 TXT     "fake answer from attacker"
diff --git a/bin/tests/system/bailiwick/ans3/victim.db b/bin/tests/system/bailiwick/ans3/victim.db
new file mode 100644 (file)
index 0000000..8f56c8d
--- /dev/null
@@ -0,0 +1,16 @@
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0.  If a copy of the MPL was not distributed with this
+; file, you can obtain one at https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+$TTL 3600
+@                      SOA     ns.attacker. . 0 0 0 0 3600
+@                      NS      ns.attacker.
+only-if-hijacked       TXT     "this record only exists in the hijacked version of the zone"
+canary                 TXT     "fake answer from attacker's auth"
diff --git a/bin/tests/system/bailiwick/bailiwick_ans.py b/bin/tests/system/bailiwick/bailiwick_ans.py
new file mode 100644 (file)
index 0000000..28353a0
--- /dev/null
@@ -0,0 +1,102 @@
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0.  If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+from typing import Dict, List, Optional, Type
+
+import abc
+
+import dns.name
+import dns.rcode
+import dns.rdatatype
+
+from isctest.asyncserver import (
+    ControlCommand,
+    ControllableAsyncDnsServer,
+    DnsProtocol,
+    QueryContext,
+    ResponseHandler,
+)
+
+
+class ResponseSpoofer(ResponseHandler, abc.ABC):
+
+    spoofers: Dict[str, Type["ResponseSpoofer"]] = {}
+
+    def __init_subclass__(cls, mode: str) -> None:
+        assert mode not in cls.spoofers
+        cls.spoofers[mode] = cls
+
+    @classmethod
+    def get_spoofer(cls, mode: str) -> Optional["ResponseSpoofer"]:
+        try:
+            return cls.spoofers[mode]()
+        except KeyError:
+            return None
+
+    @property
+    @abc.abstractmethod
+    def qname(self) -> str:
+        raise NotImplementedError
+
+    def match(self, qctx: QueryContext) -> bool:
+        return (
+            qctx.qname == dns.name.from_text(self.qname)
+            and qctx.qtype == dns.rdatatype.TXT
+            and qctx.protocol == DnsProtocol.UDP
+        )
+
+
+class SetSpoofingModeCommand(ControlCommand):
+    """
+    Select the ResponseSpoofer to use while handling queries from the resolver
+    under test (ns4).  This control command is used at the start of each test
+    function in tests_bailiwick.py.
+    """
+
+    control_subdomain = "set-spoofing-mode"
+
+    def __init__(self) -> None:
+        self._current_handler: Optional[ResponseSpoofer] = None
+
+    def handle(
+        self, args: List[str], server: ControllableAsyncDnsServer, qctx: QueryContext
+    ) -> Optional[str]:
+        if len(args) != 1:
+            qctx.response.set_rcode(dns.rcode.SERVFAIL)
+            return "invalid control command"
+
+        mode = args[0]
+
+        if mode == "none":
+            if self._current_handler:
+                server.uninstall_response_handler(self._current_handler)
+                self._current_handler = None
+            return "response spoofing disabled"
+
+        spoofer = ResponseSpoofer.get_spoofer(mode)
+        if not spoofer:
+            qctx.response.set_rcode(dns.rcode.SERVFAIL)
+            return f"unknown spoofing mode {mode}"
+
+        if self._current_handler:
+            server.uninstall_response_handler(self._current_handler)
+        server.install_response_handler(spoofer)
+        self._current_handler = spoofer
+
+        return f"response spoofing enabled (mode: {mode})"
+
+
+def spoofing_server() -> ControllableAsyncDnsServer:
+    server = ControllableAsyncDnsServer(default_rcode=dns.rcode.NOERROR)
+    server.install_control_command(SetSpoofingModeCommand())
+    return server
diff --git a/bin/tests/system/bailiwick/ns4/named.conf.j2 b/bin/tests/system/bailiwick/ns4/named.conf.j2
new file mode 100644 (file)
index 0000000..449cad2
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0.  If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+options {
+       query-source address 10.53.0.4;
+       notify-source 10.53.0.4;
+       transfer-source 10.53.0.4;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.4; };
+       listen-on-v6 { none; };
+       recursion yes;
+       dnssec-validation no;
+       qname-minimization off;
+};
+
+controls {
+       inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+include "../../_common/rndc.key";
+
+zone "." {
+       type hint;
+       file "../../_common/root.hint";
+};
diff --git a/bin/tests/system/bailiwick/tests_bailiwick.py b/bin/tests/system/bailiwick/tests_bailiwick.py
new file mode 100644 (file)
index 0000000..79ee8a4
--- /dev/null
@@ -0,0 +1,79 @@
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0.  If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+from typing import Dict
+
+import dns.message
+
+import pytest
+
+# isctest.asyncserver requires dnspython >= 2.0.0
+pytest.importorskip("dns", minversion="2.0.0")
+
+import isctest
+from isctest.instance import NamedInstance
+
+
+@pytest.fixture(autouse=True)
+def autouse_flush_resolver_cache(servers: Dict[str, NamedInstance]) -> None:
+    servers["ns4"].rndc("flush")
+
+
+def set_spoofing_mode(ans1: str, ans2: str) -> None:
+    for ip, mode in (("10.53.0.1", ans1), ("10.53.0.2", ans2)):
+        msg = dns.message.make_query(f"{mode}.set-spoofing-mode._control.", "TXT")
+        res = isctest.query.tcp(msg, ip)
+        isctest.check.noerror(res)
+
+
+def prime_cache(ns4: NamedInstance) -> None:
+    msg = dns.message.make_query("prime.victim.", "TXT")
+    res = isctest.query.tcp(msg, ns4.ip)
+    isctest.check.noerror(res)
+
+    assert res.answer[0] == dns.rrset.from_text(
+        "prime.victim.",
+        0,
+        "IN",
+        "TXT",
+        '"this record is used for priming the cache of the targeted resolver"',
+    )
+
+
+def send_trigger_query(ns4: NamedInstance, qname: str) -> None:
+    msg = dns.message.make_query(qname, "TXT")
+    isctest.query.tcp(msg, ns4.ip)
+    # The contents of the resolver's response to the trigger query do not
+    # matter, so they are not checked in any way; what matters is whether the
+    # spoofed response succeeded in hijacking the "victim." domain, which is
+    # checked below.
+
+
+def check_domain_hijack(ns4: NamedInstance) -> None:
+    # Not necessary for triggering bugs, but useful for troubleshooting test
+    # behavior.
+    ns4.rndc("dumpdb -cache")
+
+    msg = dns.message.make_query("only-if-hijacked.victim.", "TXT")
+    res = isctest.query.tcp(msg, ns4.ip)
+    isctest.check.nxdomain(res)
+
+    msg = dns.message.make_query("canary.victim.", "TXT")
+    res = isctest.query.tcp(msg, ns4.ip)
+    isctest.check.noerror(res)
+
+    assert res.answer[0] == dns.rrset.from_text(
+        "canary.victim.",
+        0,
+        "IN",
+        "TXT",
+        '"correct answer from the domain under attack"',
+    )