mirror of
https://github.com/isc-projects/bind9.git
synced 2026-05-22 10:10:14 -04:00
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:
parent
51dde6ef43
commit
f14fac5a33
6 changed files with 243 additions and 0 deletions
BIN
bin/tests/system/tkeyleak/ns1/dns.keytab
Normal file
BIN
bin/tests/system/tkeyleak/ns1/dns.keytab
Normal file
Binary file not shown.
21
bin/tests/system/tkeyleak/ns1/example.db.in
Normal file
21
bin/tests/system/tkeyleak/ns1/example.db.in
Normal 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
|
||||
39
bin/tests/system/tkeyleak/ns1/named.conf.j2
Normal file
39
bin/tests/system/tkeyleak/ns1/named.conf.j2
Normal 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";
|
||||
};
|
||||
21
bin/tests/system/tkeyleak/prereq.sh
Normal file
21
bin/tests/system/tkeyleak/prereq.sh
Normal 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
|
||||
17
bin/tests/system/tkeyleak/setup.sh
Normal file
17
bin/tests/system/tkeyleak/setup.sh
Normal 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
|
||||
145
bin/tests/system/tkeyleak/tests_tkeyleak.py
Normal file
145
bin/tests/system/tkeyleak/tests_tkeyleak.py
Normal 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
|
||||
Loading…
Reference in a new issue