]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add regression test for GSS-API context leak via TKEY CONTINUE
authorOndřej Surý <ondrej@isc.org>
Fri, 20 Mar 2026 07:43:28 +0000 (08:43 +0100)
committerMichał Kępień <michal@isc.org>
Thu, 7 May 2026 11:09:18 +0000 (13:09 +0200)
Send crafted SPNEGO NegTokenInit tokens that propose the krb5
mechanism without a mechToken.  This causes gss_accept_sec_context()
to return GSS_S_CONTINUE_NEEDED, which on unfixed code leaks the
GSS context handle (~520 bytes per query).

The test verifies that the server rejects the negotiation (TKEY
error != 0, no continuation token) rather than returning a CONTINUE
response (error=0 with output token).

(cherry picked from commit 2f2fb32d737e12c817880d584145cdf85dbc8d06)

bin/tests/system/tkeyleak/ns1/dns.keytab [new file with mode: 0644]
bin/tests/system/tkeyleak/ns1/example.db.in [new file with mode: 0644]
bin/tests/system/tkeyleak/ns1/named.conf.j2 [new file with mode: 0644]
bin/tests/system/tkeyleak/prereq.sh [new file with mode: 0644]
bin/tests/system/tkeyleak/setup.sh [new file with mode: 0644]
bin/tests/system/tkeyleak/tests_tkeyleak.py [new file with mode: 0644]

diff --git a/bin/tests/system/tkeyleak/ns1/dns.keytab b/bin/tests/system/tkeyleak/ns1/dns.keytab
new file mode 100644 (file)
index 0000000..d5a09b0
Binary files /dev/null and b/bin/tests/system/tkeyleak/ns1/dns.keytab differ
diff --git a/bin/tests/system/tkeyleak/ns1/example.db.in b/bin/tests/system/tkeyleak/ns1/example.db.in
new file mode 100644 (file)
index 0000000..dd200dc
--- /dev/null
@@ -0,0 +1,21 @@
+; 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 300
+@      IN SOA  ns.example. admin.example. (
+                       1       ; serial
+                       3600    ; refresh
+                       900     ; retry
+                       604800  ; expire
+                       300     ; minimum
+               )
+@      IN NS   ns.example.
+ns     IN A    10.53.0.1
diff --git a/bin/tests/system/tkeyleak/ns1/named.conf.j2 b/bin/tests/system/tkeyleak/ns1/named.conf.j2
new file mode 100644 (file)
index 0000000..f16b534
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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.1;
+       notify-source 10.53.0.1;
+       transfer-source 10.53.0.1;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.1; };
+       listen-on-v6 { none; };
+       recursion no;
+       dnssec-validation no;
+       tkey-gssapi-keytab "dns.keytab";
+};
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "example" {
+       type primary;
+       file "example.db";
+};
diff --git a/bin/tests/system/tkeyleak/prereq.sh b/bin/tests/system/tkeyleak/prereq.sh
new file mode 100644 (file)
index 0000000..8a68ae7
--- /dev/null
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+# 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.
+
+. ../conf.sh
+
+$FEATURETEST --gssapi || {
+  echo_i "gssapi not supported - skipping tkeyleak test"
+  exit 255
+}
+
+exit 0
diff --git a/bin/tests/system/tkeyleak/setup.sh b/bin/tests/system/tkeyleak/setup.sh
new file mode 100644 (file)
index 0000000..24a0026
--- /dev/null
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+
+# 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.
+
+# shellcheck source=conf.sh
+. ../conf.sh
+
+cp ns1/example.db.in ns1/example.db
diff --git a/bin/tests/system/tkeyleak/tests_tkeyleak.py b/bin/tests/system/tkeyleak/tests_tkeyleak.py
new file mode 100644 (file)
index 0000000..fd97c85
--- /dev/null
@@ -0,0 +1,145 @@
+# 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.
+
+"""
+Regression test for GSS-API context leak via repeated TKEY queries.
+
+An unauthenticated attacker could exhaust server memory by sending
+repeated TKEY queries with crafted SPNEGO NegTokenInit tokens.
+Each query triggers gss_accept_sec_context() which returns
+GSS_S_CONTINUE_NEEDED and allocates a GSS context.  On the unfixed
+code path, the context handle in process_gsstkey() is never stored
+or freed, leaking ~520 bytes per query.
+
+The fix rejects GSS_S_CONTINUE_NEEDED in dst_gssapi_acceptctx() and
+deletes the context immediately.
+
+The key distinguishing signal in the TKEY response:
+  - CONTINUE (vulnerable): error=0, output token present, no TSIG
+  - BADKEY (fixed):        error=17, no output token
+"""
+
+import struct
+import time
+
+import dns.name
+import dns.query
+import dns.rdataclass
+import dns.rdatatype
+import dns.rdtypes.ANY.TKEY
+import pytest
+
+import isctest
+
+pytestmark = pytest.mark.extra_artifacts(
+    [
+        "*/*.db",
+    ]
+)
+
+TKEY_NAME = dns.name.from_text("test.key.")
+GSSAPI_ALGORITHM = dns.name.from_text("gss-tsig.")
+TKEY_MODE_GSSAPI = 3
+
+# OID 1.2.840.113554.1.2.2 (Kerberos 5)
+KRB5_OID = b"\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02"
+
+# OID 1.3.6.1.5.5.2 (SPNEGO)
+SPNEGO_OID = b"\x06\x06\x2b\x06\x01\x05\x05\x02"
+
+
+def der_encode(tag, data):
+    """Encode data in ASN.1 DER TLV format."""
+    length = len(data)
+    if length < 128:
+        return tag + bytes([length]) + data
+    if length < 256:
+        return tag + b"\x81" + bytes([length]) + data
+    return tag + b"\x82" + struct.pack(">H", length) + data
+
+
+def spnego_negtokeninit():
+    """Build a SPNEGO NegTokenInit proposing krb5 without a mechToken.
+
+    This forces gss_accept_sec_context() to return GSS_S_CONTINUE_NEEDED
+    because the acceptor recognizes the krb5 mechanism but has not
+    received an actual AP-REQ token yet.
+    """
+    # MechTypeList ::= SEQUENCE OF MechType
+    mechtype_list = der_encode(b"\x30", KRB5_OID)
+    # [0] mechTypes
+    mechtypes = der_encode(b"\xa0", mechtype_list)
+    # NegTokenInit ::= SEQUENCE { mechTypes, ... }
+    negtokeninit = der_encode(b"\x30", mechtypes)
+    # [0] CONSTRUCTED (wrapping NegTokenInit)
+    wrapped = der_encode(b"\xa0", negtokeninit)
+    # APPLICATION 0 CONSTRUCTED (SPNEGO OID + body)
+    return der_encode(b"\x60", SPNEGO_OID + wrapped)
+
+
+def make_tkey_query(token):
+    """Build a TKEY query with a GSS-API token in the additional section."""
+    now = int(time.time())
+    tkey_rdata = dns.rdtypes.ANY.TKEY.TKEY(
+        rdclass=dns.rdataclass.ANY,
+        rdtype=dns.rdatatype.TKEY,
+        algorithm=GSSAPI_ALGORITHM,
+        inception=now,
+        expiration=now + 86400,
+        mode=TKEY_MODE_GSSAPI,
+        error=0,
+        key=token,
+        other=b"",
+    )
+
+    msg = isctest.query.create(TKEY_NAME, dns.rdatatype.TKEY, dns.rdataclass.ANY)
+    rrset = msg.find_rrset(
+        msg.additional,
+        TKEY_NAME,
+        dns.rdataclass.ANY,
+        dns.rdatatype.TKEY,
+        create=True,
+    )
+    rrset.add(tkey_rdata)
+    return msg
+
+
+def test_tkey_gssapi_no_continuation(ns1):
+    """TKEY with a SPNEGO NegTokenInit must be rejected, not continued.
+
+    On unfixed code, gss_accept_sec_context() returns CONTINUE_NEEDED
+    and the response has error=0 with an output token (the leaked path).
+    On fixed code, CONTINUE_NEEDED is rejected and the response has
+    error=BADKEY(17) with no output token.
+    """
+    port = ns1.ports.dns
+    ip = ns1.ip
+
+    msg = make_tkey_query(spnego_negtokeninit())
+    res = dns.query.tcp(msg, ip, port=port, timeout=5)
+
+    assert res is not None
+
+    tkey = get_tkey_answer(res)
+    assert tkey is not None, "server did not return a TKEY answer"
+    assert (
+        tkey.error != 0
+    ), "server returned error=0 (GSS_S_CONTINUE_NEEDED not rejected)"
+    assert len(tkey.key) == 0, "server returned a continuation token"
+
+
+def get_tkey_answer(response):
+    """Extract TKEY rdata from a DNS response, or None."""
+    for rrset in response.answer:
+        if rrset.rdtype == dns.rdatatype.TKEY:
+            for rdata in rrset:
+                return rdata
+    return None