From 41c8a42c8ae8b89354ddaa02ea3d0035445d6b44 Mon Sep 17 00:00:00 2001 From: Jo Sutton Date: Thu, 27 Jun 2024 12:29:52 +1200 Subject: [PATCH] tests/krb5: Add tests for errors produced when logging in with unusable accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Heimdal matches Windows in the no‐FAST case, but produces NTSTATUS codes when it shouldn’t in the FAST case. BUG: https://bugzilla.samba.org/show_bug.cgi?id=15655 Signed-off-by: Jo Sutton Reviewed-by: Douglas Bagnall (cherry picked from commit c5ee0b60b20011aeaa60c2f549c2a78269c97c8f) --- python/samba/tests/krb5/lockout_tests.py | 210 ++++++++++++++++++++++- selftest/knownfail_heimdal_kdc | 8 + selftest/knownfail_mit_kdc | 5 + 3 files changed, 221 insertions(+), 2 deletions(-) diff --git a/python/samba/tests/krb5/lockout_tests.py b/python/samba/tests/krb5/lockout_tests.py index d91eb1df79d..c5d8dff6a2f 100755 --- a/python/samba/tests/krb5/lockout_tests.py +++ b/python/samba/tests/krb5/lockout_tests.py @@ -58,11 +58,12 @@ from samba.tests import connect_samdb, env_get_var_value, env_loadparm from samba.tests.krb5.as_req_tests import AsReqBaseTest from samba.tests.krb5 import kcrypto -from samba.tests.krb5.kdc_base_test import KDCBaseTest +from samba.tests.krb5.kdc_tgs_tests import KdcTgsBaseTests from samba.tests.krb5.raw_testcase import KerberosCredentials import samba.tests.krb5.rfc4120_pyasn1 as krb5_asn1 from samba.tests.krb5.rfc4120_constants import ( KDC_ERR_CLIENT_REVOKED, + KDC_ERR_KEY_EXPIRED, KDC_ERR_PREAUTH_FAILED, KRB_AS_REP, KRB_ERROR, @@ -518,7 +519,7 @@ def ldap_pwd_change(pipe, return ConnectionResult.SUCCESS -class LockoutTests(KDCBaseTest): +class LockoutTests(KdcTgsBaseTests): def setUp(self): super().setUp() @@ -611,6 +612,211 @@ class LockoutTests(KDCBaseTest): def test_lockout_transaction_kdc_ntstatus(self): self.do_lockout_transaction(partial(connect_kdc, expect_status=True)) + # Test that performing AS‐REQs with accounts in various states of + # unusability results in appropriate NTSTATUS and Kerberos error codes. + + def test_lockout_status_disabled(self): + self._run_lockout_status( + self._get_creds_disabled(), + expected_status=ntstatus.NT_STATUS_ACCOUNT_DISABLED, + expected_error=KDC_ERR_CLIENT_REVOKED, + ) + + def test_lockout_status_locked_out(self): + self._run_lockout_status( + self._get_creds_locked_out(), + expected_status=ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT, + expected_error=KDC_ERR_CLIENT_REVOKED, + ) + + def test_lockout_status_expired(self): + self._run_lockout_status( + self._get_creds_expired(), + expected_status=ntstatus.NT_STATUS_ACCOUNT_EXPIRED, + expected_error=KDC_ERR_CLIENT_REVOKED, + ) + + def test_lockout_status_must_change(self): + self._run_lockout_status( + self._get_creds_must_change(), + expected_status=ntstatus.NT_STATUS_PASSWORD_MUST_CHANGE, + expected_error=KDC_ERR_KEY_EXPIRED, + ) + + def test_lockout_status_password_expired(self): + self._run_lockout_status( + self._get_creds_password_expired(), + expected_status=ntstatus.NT_STATUS_PASSWORD_EXPIRED, + expected_error=KDC_ERR_KEY_EXPIRED, + ) + + # Test that performing the same AS‐REQs, this time with FAST, does not + # result in NTSTATUS codes. + + def test_lockout_status_disabled_fast(self): + self._run_lockout_status_fast( + self._get_creds_disabled(), expected_error=KDC_ERR_CLIENT_REVOKED + ) + + def test_lockout_status_locked_out_fast(self): + self._run_lockout_status_fast( + self._get_creds_locked_out(), expected_error=KDC_ERR_CLIENT_REVOKED + ) + + def test_lockout_status_expired_fast(self): + self._run_lockout_status_fast( + self._get_creds_expired(), expected_error=KDC_ERR_CLIENT_REVOKED + ) + + def test_lockout_status_must_change_fast(self): + self._run_lockout_status_fast( + self._get_creds_must_change(), expected_error=KDC_ERR_KEY_EXPIRED + ) + + def test_lockout_status_password_expired_fast(self): + self._run_lockout_status_fast( + self._get_creds_password_expired(), expected_error=KDC_ERR_KEY_EXPIRED + ) + + def _get_creds_disabled(self): + return self.get_cached_creds( + account_type=self.AccountType.USER, opts={"enabled": False} + ) + + def _get_creds_locked_out(self) -> KerberosCredentials: + samdb = self.get_samdb() + + user_creds = self.get_cached_creds( + account_type=self.AccountType.USER, use_cache=False + ) + user_dn = user_creds.get_dn() + + # Lock out the account. + + old_utf16pw = '"Secret007"'.encode("utf-16le") # invalid pwd + new_utf16pw = '"Secret008"'.encode("utf-16le") + + msg = ldb.Message(user_dn) + msg["0"] = ldb.MessageElement(old_utf16pw, ldb.FLAG_MOD_DELETE, "unicodePwd") + msg["1"] = ldb.MessageElement(new_utf16pw, ldb.FLAG_MOD_ADD, "unicodePwd") + + for _ in range(self.lockout_threshold): + try: + samdb.modify(msg) + except ldb.LdbError as err: + num, _ = err.args + + # We get an error, but the bad password count should + # still be updated. + self.assertEqual(num, ldb.ERR_CONSTRAINT_VIOLATION) + else: + self.fail("pwd change should have failed") + + # Ensure the account is locked out. + + res = samdb.search( + user_dn, scope=ldb.SCOPE_BASE, attrs=["msDS-User-Account-Control-Computed"] + ) + self.assertEqual(1, len(res)) + + uac = int(res[0].get("msDS-User-Account-Control-Computed", idx=0)) + self.assertTrue(uac & dsdb.UF_LOCKOUT) + + return user_creds + + def _get_creds_expired(self) -> KerberosCredentials: + return self.get_cached_creds( + account_type=self.AccountType.USER, + opts={"additional_details": self.freeze({"accountExpires": "1"})}, + ) + + def _get_creds_must_change(self) -> KerberosCredentials: + return self.get_cached_creds( + account_type=self.AccountType.USER, + opts={"additional_details": self.freeze({"pwdLastSet": "0"})}, + ) + + def _get_creds_password_expired(self) -> KerberosCredentials: + samdb = self.get_samdb() + self.addCleanup(samdb.set_maxPwdAge, samdb.get_maxPwdAge()) + low_pwd_age = -2 + samdb.set_maxPwdAge(low_pwd_age) + + return self.get_cached_creds(account_type=self.AccountType.USER) + + def _run_lockout_status( + self, + user_creds: KerberosCredentials, + *, + expected_status: int, + expected_error: int, + ) -> None: + user_name = user_creds.get_username() + cname = self.PrincipalName_create( + name_type=NT_PRINCIPAL, names=user_name.split("/") + ) + + krbtgt_creds = self.get_krbtgt_creds() + realm = krbtgt_creds.get_realm() + + sname = self.get_krbtgt_sname() + + preauth_key = self.PasswordKey_from_creds(user_creds, kcrypto.Enctype.AES256) + + ts_enc_padata = self.get_enc_timestamp_pa_data_from_key(preauth_key) + padata = [ts_enc_padata] + + def _generate_padata_copy(_kdc_exchange_dict, _callback_dict, req_body): + return padata, req_body + + kdc_exchange_dict = self.as_exchange_dict( + creds=user_creds, + expected_crealm=realm, + expected_cname=cname, + expected_srealm=realm, + expected_sname=sname, + expected_account_name=user_name, + expected_supported_etypes=krbtgt_creds.tgs_supported_enctypes, + expect_edata=True, + expect_status=True, + expected_status=expected_status, + ticket_decryption_key=self.TicketDecryptionKey_from_creds(krbtgt_creds), + generate_padata_fn=_generate_padata_copy, + check_error_fn=self.generic_check_kdc_error, + check_rep_fn=None, + check_kdc_private_fn=self.generic_check_kdc_private, + expected_error_mode=expected_error, + expected_salt=user_creds.get_salt(), + preauth_key=preauth_key, + kdc_options=str(krb5_asn1.KDCOptions("postdated")), + pac_request=True, + ) + + # Try making a Kerberos AS-REQ to the KDC. This might fail, either due + # to the user's account being locked out or due to using the wrong + # password. + self._generic_kdc_exchange( + kdc_exchange_dict, + cname=cname, + realm=realm, + sname=sname, + till_time=self.get_KerberosTime(offset=36000), + etypes=self.get_default_enctypes(user_creds), + ) + + def _run_lockout_status_fast( + self, user_creds: KerberosCredentials, *, expected_error: int + ) -> None: + self._armored_as_req( + user_creds, + self.get_krbtgt_creds(), + self.get_tgt(self.get_mach_creds()), + expected_error=expected_error, + expect_edata=self.expect_padata_outer, + # FAST‐armored responses never contain an NTSTATUS code. + expect_status=False, + ) + def test_lockout_transaction_ntlm(self): self.do_lockout_transaction(connect_ntlm) diff --git a/selftest/knownfail_heimdal_kdc b/selftest/knownfail_heimdal_kdc index 811d3202729..b3df84e8959 100644 --- a/selftest/knownfail_heimdal_kdc +++ b/selftest/knownfail_heimdal_kdc @@ -156,3 +156,11 @@ ^samba\.tests\.krb5\.conditional_ace_tests\.samba\.tests\.krb5\.conditional_ace_tests\.TgsReqServicePolicyTests\.test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_claims_target_policy\(ad_dc\)$ ^samba\.tests\.krb5\.conditional_ace_tests\.samba\.tests\.krb5\.conditional_ace_tests\.TgsReqServicePolicyTests\.test_pac_device_info_no_compound_id_support_no_claims_valid_existing_device_claims\(ad_dc\)$ ^samba\.tests\.krb5\.conditional_ace_tests\.samba\.tests\.krb5\.conditional_ace_tests\.TgsReqServicePolicyTests\.test_pac_device_info_rodc_issued\(ad_dc\)$ +# +# Lockout tests +# +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_disabled_fast\(ad_dc:local\)$ +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_expired_fast\(ad_dc:local\)$ +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_locked_out_fast\(ad_dc:local\)$ +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_must_change_fast\(ad_dc:local\)$ +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_password_expired_fast\(ad_dc:local\)$ diff --git a/selftest/knownfail_mit_kdc b/selftest/knownfail_mit_kdc index 7256fb3f110..6b67251a404 100644 --- a/selftest/knownfail_mit_kdc +++ b/selftest/knownfail_mit_kdc @@ -532,6 +532,11 @@ samba.tests.krb5.as_canonicalization_tests.samba.tests.krb5.as_canonicalization_ ^samba.tests.krb5.lockout_tests.samba.tests.krb5.lockout_tests.LockoutTests.test_lockout_transaction_kdc_ntstatus.ad_dc:local ^samba.tests.krb5.lockout_tests.samba.tests.krb5.lockout_tests.LockoutTests.test_lockout_transaction_rename_kdc.ad_dc:local ^samba.tests.krb5.lockout_tests.samba.tests.krb5.lockout_tests.LockoutTests.test_lockout_transaction_rename_kdc_ntstatus.ad_dc:local +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_disabled\(ad_dc:local\)$ +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_expired\(ad_dc:local\)$ +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_locked_out\(ad_dc:local\)$ +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_must_change\(ad_dc:local\)$ +^samba\.tests\.krb5\.lockout_tests\.samba\.tests\.krb5\.lockout_tests\.LockoutTests\.test_lockout_status_password_expired\(ad_dc:local\)$ # # Encryption type tests #