Add regression test for GSS-API context leak via TKEY CONTINUE

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).
This commit is contained in:
Ondřej Surý 2026-03-20 08:43:28 +01:00 committed by Michał Kępień
parent 51dde6ef43
commit f14fac5a33
No known key found for this signature in database
6 changed files with 243 additions and 0 deletions

Binary file not shown.

View file

@ -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

View file

@ -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";
};

View file

@ -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

View file

@ -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

View file

@ -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