From: Petr Špaček Date: Fri, 11 Jul 2025 16:37:57 +0000 (+0200) Subject: Add a common base for CVE-2025-40778 tests X-Git-Tag: v9.21.17~14^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=607974b1bcb08334f2f5ab0efcc069ba69eb3130;p=thirdparty%2Fbind9.git Add a common base for CVE-2025-40778 tests 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ń --- diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dfb583ce5cb..243a98c88fd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 index 00000000000..be072a39e16 --- /dev/null +++ b/bin/tests/system/bailiwick/ans1/ans.py @@ -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 index 00000000000..64693727b68 --- /dev/null +++ b/bin/tests/system/bailiwick/ans1/root.db @@ -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 index 00000000000..be072a39e16 --- /dev/null +++ b/bin/tests/system/bailiwick/ans2/ans.py @@ -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 index 00000000000..d5b4d258f4f --- /dev/null +++ b/bin/tests/system/bailiwick/ans2/victim.db @@ -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 index 00000000000..2416f57e654 --- /dev/null +++ b/bin/tests/system/bailiwick/ans3/ans.py @@ -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 index 00000000000..e7474bbf30c --- /dev/null +++ b/bin/tests/system/bailiwick/ans3/attacker.db @@ -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 index 00000000000..8f56c8d29d3 --- /dev/null +++ b/bin/tests/system/bailiwick/ans3/victim.db @@ -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 index 00000000000..28353a0a099 --- /dev/null +++ b/bin/tests/system/bailiwick/bailiwick_ans.py @@ -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 index 00000000000..449cad22e6f --- /dev/null +++ b/bin/tests/system/bailiwick/ns4/named.conf.j2 @@ -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 index 00000000000..79ee8a4364c --- /dev/null +++ b/bin/tests/system/bailiwick/tests_bailiwick.py @@ -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"', + )