From 894be09a93cd99cea2164fe8ec82c97c91b277c5 Mon Sep 17 00:00:00 2001 From: Joseph Sutton Date: Thu, 21 Oct 2021 15:45:00 +1300 Subject: [PATCH] CVE-2020-25722 tests/krb5: Add KDC tests for 3-part SPNs BUG: https://bugzilla.samba.org/show_bug.cgi?id=14776 Signed-off-by: Joseph Sutton Reviewed-by: Andrew Bartlett --- python/samba/tests/krb5/kdc_base_test.py | 1 + python/samba/tests/krb5/spn_tests.py | 212 +++++++++++++++++++++++ python/samba/tests/usage.py | 1 + selftest/knownfail_heimdal_kdc | 6 + selftest/knownfail_mit_kdc | 6 + source4/selftest/tests.py | 10 ++ 6 files changed, 236 insertions(+) create mode 100755 python/samba/tests/krb5/spn_tests.py diff --git a/python/samba/tests/krb5/kdc_base_test.py b/python/samba/tests/krb5/kdc_base_test.py index aed4c427ab0..cc23484ba2c 100644 --- a/python/samba/tests/krb5/kdc_base_test.py +++ b/python/samba/tests/krb5/kdc_base_test.py @@ -97,6 +97,7 @@ class KDCBaseTest(RawKerberosTest): USER = auto() COMPUTER = auto() SERVER = auto() + RODC = auto() @classmethod def setUpClass(cls): diff --git a/python/samba/tests/krb5/spn_tests.py b/python/samba/tests/krb5/spn_tests.py new file mode 100755 index 00000000000..62d2ea081bc --- /dev/null +++ b/python/samba/tests/krb5/spn_tests.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# Unix SMB/CIFS implementation. +# Copyright (C) Stefan Metzmacher 2020 +# Copyright (C) 2020 Catalyst.Net Ltd +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import os +import sys + +from samba.tests import DynamicTestCase + +import ldb + +from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.raw_testcase import KerberosCredentials +from samba.tests.krb5.rfc4120_constants import ( + AES256_CTS_HMAC_SHA1_96, + ARCFOUR_HMAC_MD5, + KDC_ERR_S_PRINCIPAL_UNKNOWN, + NT_PRINCIPAL, +) + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +global_asn1_print = False +global_hexdump = False + + +@DynamicTestCase +class SpnTests(KDCBaseTest): + test_account_types = { + 'computer': KDCBaseTest.AccountType.COMPUTER, + 'server': KDCBaseTest.AccountType.SERVER, + 'rodc': KDCBaseTest.AccountType.RODC + } + test_spns = { + '2_part': 'ldap/{{account}}', + '3_part_our_domain': 'ldap/{{account}}/{netbios_domain_name}', + '3_part_our_realm': 'ldap/{{account}}/{dns_domain_name}', + '3_part_not_our_realm': 'ldap/{{account}}/test', + '3_part_instance': 'ldap/{{account}}:test/{dns_domain_name}' + } + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls._mock_rodc_creds = None + + @classmethod + def setUpDynamicTestCases(cls): + for account_type_name, account_type in cls.test_account_types.items(): + for spn_name, spn in cls.test_spns.items(): + tname = f'{spn_name}_spn_{account_type_name}' + targs = (account_type, spn) + cls.generate_dynamic_test('test_spn', tname, *targs) + + def _test_spn_with_args(self, account_type, spn): + target_creds = self._get_creds(account_type) + spn = self._format_spn(spn, target_creds) + + sname = self.PrincipalName_create(name_type=NT_PRINCIPAL, + names=spn.split('/')) + + client_creds = self.get_client_creds() + tgt = self.get_tgt(client_creds) + + samdb = self.get_samdb() + netbios_domain_name = samdb.domain_netbios_name() + dns_domain_name = samdb.domain_dns_name() + + subkey = self.RandomKey(tgt.session_key.etype) + + etypes = (AES256_CTS_HMAC_SHA1_96, ARCFOUR_HMAC_MD5,) + + if account_type is self.AccountType.SERVER: + ticket_etype = AES256_CTS_HMAC_SHA1_96 + else: + ticket_etype = None + decryption_key = self.TicketDecryptionKey_from_creds( + target_creds, etype=ticket_etype) + + if (spn.count('/') > 1 + and (spn.endswith(netbios_domain_name) + or spn.endswith(dns_domain_name)) + and account_type is not self.AccountType.SERVER + and account_type is not self.AccountType.RODC): + expected_error_mode = KDC_ERR_S_PRINCIPAL_UNKNOWN + check_error_fn = self.generic_check_kdc_error + check_rep_fn = None + else: + expected_error_mode = 0 + check_error_fn = None + check_rep_fn = self.generic_check_kdc_rep + + kdc_exchange_dict = self.tgs_exchange_dict( + expected_crealm=tgt.crealm, + expected_cname=tgt.cname, + expected_srealm=tgt.srealm, + expected_sname=sname, + ticket_decryption_key=decryption_key, + check_rep_fn=check_rep_fn, + check_error_fn=check_error_fn, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error_mode, + tgt=tgt, + authenticator_subkey=subkey, + kdc_options='0', + expect_edata=False) + + self._generic_kdc_exchange(kdc_exchange_dict, + cname=None, + realm=tgt.srealm, + sname=sname, + etypes=etypes) + + def setUp(self): + super().setUp() + self.do_asn1_print = global_asn1_print + self.do_hexdump = global_hexdump + + def _format_spns(self, spns, creds=None): + return map(lambda spn: self._format_spn(spn, creds), spns) + + def _format_spn(self, spn, creds=None): + samdb = self.get_samdb() + + spn = spn.format(netbios_domain_name=samdb.domain_netbios_name(), + dns_domain_name=samdb.domain_dns_name()) + + if creds is not None: + account_name = creds.get_username() + spn = spn.format(account=account_name) + + return spn + + def _get_creds(self, account_type): + spns = self._format_spns(self.test_spns.values()) + + if account_type is self.AccountType.RODC: + creds = self._mock_rodc_creds + if creds is None: + creds = self._get_mock_rodc_creds(spns) + type(self)._mock_rodc_creds = creds + else: + creds = self.get_cached_creds( + account_type=account_type, + opts={ + 'spn': spns + }) + + return creds + + def _get_mock_rodc_creds(self, spns): + rodc_ctx = self.get_mock_rodc_ctx() + + for spn in spns: + spn = spn.format(account=rodc_ctx.myname) + if spn not in rodc_ctx.SPNs: + rodc_ctx.SPNs.append(spn) + + samdb = self.get_samdb() + rodc_dn = ldb.Dn(samdb, rodc_ctx.acct_dn) + + msg = ldb.Message(rodc_dn) + msg['servicePrincipalName'] = ldb.MessageElement( + rodc_ctx.SPNs, + ldb.FLAG_MOD_REPLACE, + 'servicePrincipalName') + samdb.modify(msg) + + creds = KerberosCredentials() + creds.guess(self.get_lp()) + creds.set_realm(rodc_ctx.realm.upper()) + creds.set_domain(rodc_ctx.domain_name) + creds.set_password(rodc_ctx.acct_pass) + creds.set_username(rodc_ctx.myname) + creds.set_workstation(rodc_ctx.samname) + creds.set_dn(rodc_dn) + creds.set_spn(rodc_ctx.SPNs) + + res = samdb.search(base=rodc_dn, + scope=ldb.SCOPE_BASE, + attrs=['msDS-KeyVersionNumber']) + kvno = int(res[0].get('msDS-KeyVersionNumber', idx=0)) + creds.set_kvno(kvno) + + keys = self.get_keys(samdb, rodc_dn) + self.creds_set_keys(creds, keys) + + return creds + + +if __name__ == "__main__": + global_asn1_print = False + global_hexdump = False + import unittest + unittest.main() diff --git a/python/samba/tests/usage.py b/python/samba/tests/usage.py index 4b68a2b798c..7d11b6b4617 100644 --- a/python/samba/tests/usage.py +++ b/python/samba/tests/usage.py @@ -104,6 +104,7 @@ EXCLUDE_USAGE = { 'python/samba/tests/krb5/fast_tests.py', 'python/samba/tests/krb5/rodc_tests.py', 'python/samba/tests/krb5/salt_tests.py', + 'python/samba/tests/krb5/spn_tests.py', } EXCLUDE_HELP = { diff --git a/selftest/knownfail_heimdal_kdc b/selftest/knownfail_heimdal_kdc index b39b11c3c53..45524d70fa2 100644 --- a/selftest/knownfail_heimdal_kdc +++ b/selftest/knownfail_heimdal_kdc @@ -255,3 +255,9 @@ ^samba.tests.krb5.s4u_tests.samba.tests.krb5.s4u_tests.S4UKerberosTests.test_rbcd_no_auth_data_required ^samba.tests.krb5.s4u_tests.samba.tests.krb5.s4u_tests.S4UKerberosTests.test_rbcd_no_client_pac_no_auth_data_required_a ^samba.tests.krb5.s4u_tests.samba.tests.krb5.s4u_tests.S4UKerberosTests.test_rbcd_no_client_pac_no_auth_data_required_b +# +# SPN tests +# +^samba.tests.krb5.spn_tests.samba.tests.krb5.spn_tests.SpnTests.test_spn_3_part_instance_spn_computer +^samba.tests.krb5.spn_tests.samba.tests.krb5.spn_tests.SpnTests.test_spn_3_part_our_domain_spn_computer +^samba.tests.krb5.spn_tests.samba.tests.krb5.spn_tests.SpnTests.test_spn_3_part_our_realm_spn_computer diff --git a/selftest/knownfail_mit_kdc b/selftest/knownfail_mit_kdc index 4fc68ffd854..c86f9c2c2ea 100644 --- a/selftest/knownfail_mit_kdc +++ b/selftest/knownfail_mit_kdc @@ -374,3 +374,9 @@ samba.tests.krb5.as_canonicalization_tests.samba.tests.krb5.as_canonicalization_ ^samba4.rpc.pac on ncacn_np.netr-mem-arcfour.verify-sig-arcfour.fl2003dc ^samba4.rpc.pac on ncacn_np.netr-mem-arcfour.verify-sig-arcfour.fl2008dc ^samba4.rpc.pac on ncacn_np.netr-mem-arcfour.verify-sig-arcfour.fl2008r2dc +# +# SPN tests +# +^samba.tests.krb5.spn_tests.samba.tests.krb5.spn_tests.SpnTests.test_spn_3_part_instance_spn_computer +^samba.tests.krb5.spn_tests.samba.tests.krb5.spn_tests.SpnTests.test_spn_3_part_our_domain_spn_computer +^samba.tests.krb5.spn_tests.samba.tests.krb5.spn_tests.SpnTests.test_spn_3_part_our_realm_spn_computer diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index 640b085a922..44bb50267c4 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -1468,6 +1468,16 @@ planpythontestsuite( 'FAST_SUPPORT': have_fast_support, 'TKT_SIG_SUPPORT': tkt_sig_support }) +planpythontestsuite( + "ad_dc", + "samba.tests.krb5.spn_tests", + environ={ + 'ADMIN_USERNAME': '$USERNAME', + 'ADMIN_PASSWORD': '$PASSWORD', + 'STRICT_CHECKING': '0', + 'FAST_SUPPORT': have_fast_support, + 'TKT_SIG_SUPPORT': tkt_sig_support + }) for env in [ 'vampire_dc', -- 2.47.2