]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
tests/krb5: Improve _test_samr_change_password() method
authorJoseph Sutton <josephsutton@catalyst.net.nz>
Sun, 30 Apr 2023 21:48:15 +0000 (09:48 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Fri, 5 May 2023 02:54:31 +0000 (02:54 +0000)
Instead of using anonymous credentials, we now connect using the
passed-in credentials.

We now correctly construct nt_password and nt_verifier so as to
successfully change the password, instead of having to distinguish
between a WRONG_PASSWORD error and an error caused by the password
change being disallowed.

Signed-off-by: Joseph Sutton <josephsutton@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/tests/krb5/kdc_base_test.py
python/samba/tests/krb5/protected_users_tests.py

index bf0e6642527c20584799f86b80b74e925c3752c6..37c278e01dc4439096153001e2d83f2979593709 100644 (file)
@@ -34,7 +34,13 @@ from enum import Enum
 from collections import namedtuple
 import ldb
 from ldb import SCOPE_BASE
-from samba import NTSTATUSError, common, generate_random_password, ntstatus
+from samba import (
+    NTSTATUSError,
+    arcfour_encrypt,
+    common,
+    generate_random_password,
+    ntstatus,
+)
 from samba.auth import system_session
 from samba.credentials import (
     Credentials,
@@ -42,6 +48,7 @@ from samba.credentials import (
     DONT_USE_KERBEROS,
     MUST_USE_KERBEROS,
 )
+from samba.crypto import des_crypt_blob_16, md4_hash_blob
 from samba.dcerpc import (
     claims,
     drsblobs,
@@ -3171,51 +3178,132 @@ class KDCBaseTest(RawKerberosTest):
 
     # Test the two SAMR password change methods implemented in Samba. If the
     # user is protected, we should get an ACCOUNT_RESTRICTION error indicating
-    # that the password change is not allowed; otherwise we should get a
-    # WRONG_PASSWORD error.
-    def _test_samr_change_password(self, creds, expect_error):
+    # that the password change is not allowed.
+    def _test_samr_change_password(self, creds, expect_error,
+                                   connect_error=None):
         samdb = self.get_samdb()
         server_name = samdb.host_dns_name()
-        conn = samr.samr(f'ncacn_np:{server_name}[krb5,seal,smb2]')
+        try:
+            conn = samr.samr(f'ncacn_np:{server_name}[seal,smb2]',
+                             self.get_lp(),
+                             creds)
+        except NTSTATUSError as err:
+            self.assertIsNotNone(connect_error,
+                                 'connection unexpectedly failed')
+            self.assertIsNone(expect_error, 'don’t specify both errors')
+
+            num, _ = err.args
+            self.assertEqual(num, connect_error)
+
+            return
+        else:
+            self.assertIsNone(connect_error, 'expected connection to fail')
+
+        # Get the NT hash.
+        nt_hash = creds.get_nt_hash()
 
-        username = creds.get_username()
+        # Generate a new UTF-16 password.
+        new_password_str = generate_random_password(32, 32)
+        new_password = new_password_str.encode('utf-16le')
+
+        # Generate the MD4 hash of the password.
+        new_password_md4 = md4_hash_blob(new_password)
+
+        # Prefix the password with padding so it is 512 bytes long.
+        new_password_len = len(new_password)
+        remaining_len = 512 - new_password_len
+        new_password = bytes(remaining_len) + new_password
+
+        # Append the 32-bit length of the password.
+        new_password += int.to_bytes(new_password_len,
+                                     length=4,
+                                     byteorder='little')
+
+        # Create a key from the MD4 hash of the new password.
+        key = new_password_md4[:14]
+
+        # Encrypt the old NT hash with DES to obtain the verifier.
+        verifier = des_crypt_blob_16(nt_hash, key)
 
         server = lsa.String()
         server.string = server_name
 
         account = lsa.String()
-        account.string = username
+        account.string = creds.get_username()
 
-        nt_password = samr.CryptPassword()
         nt_verifier = samr.Password()
+        nt_verifier.hash = list(verifier)
+
+        nt_password = samr.CryptPassword()
+        nt_password.data = list(arcfour_encrypt(nt_hash, new_password))
 
         if not self.expect_nt_hash:
             expect_error = ntstatus.NT_STATUS_NTLM_BLOCKED
 
-        with self.assertRaises(NTSTATUSError) as err:
+        try:
             conn.ChangePasswordUser2(server=server,
                                      account=account,
                                      nt_password=nt_password,
                                      nt_verifier=nt_verifier,
-                                     lm_change=True,
+                                     lm_change=False,
                                      lm_password=None,
                                      lm_verifier=None)
+        except NTSTATUSError as err:
+            num, _ = err.args
+            self.assertIsNotNone(expect_error,
+                                 f'unexpectedly failed with {num:08X}')
+            self.assertEqual(num, expect_error)
+        else:
+            self.assertIsNone(expect_error, 'expected to fail')
+
+        creds.set_password(new_password_str)
+
+        # Get the NT hash.
+        nt_hash = creds.get_nt_hash()
+
+        # Generate a new UTF-16 password.
+        new_password = generate_random_password(32, 32)
+        new_password = new_password.encode('utf-16le')
+
+        # Generate the MD4 hash of the password.
+        new_password_md4 = md4_hash_blob(new_password)
+
+        # Prefix the password with padding so it is 512 bytes long.
+        new_password_len = len(new_password)
+        remaining_len = 512 - new_password_len
+        new_password = bytes(remaining_len) + new_password
 
-        num, _ = err.exception.args
-        self.assertEqual(num, expect_error)
+        # Append the 32-bit length of the password.
+        new_password += int.to_bytes(new_password_len,
+                                     length=4,
+                                     byteorder='little')
 
-        with self.assertRaises(NTSTATUSError) as err:
+        # Create a key from the MD4 hash of the new password.
+        key = new_password_md4[:14]
+
+        # Encrypt the old NT hash with DES to obtain the verifier.
+        verifier = des_crypt_blob_16(nt_hash, key)
+
+        nt_verifier.hash = list(verifier)
+
+        nt_password.data = list(arcfour_encrypt(nt_hash, new_password))
+
+        try:
             conn.ChangePasswordUser3(server=server,
                                      account=account,
                                      nt_password=nt_password,
                                      nt_verifier=nt_verifier,
-                                     lm_change=True,
+                                     lm_change=False,
                                      lm_password=None,
                                      lm_verifier=None,
                                      password3=None)
+        except NTSTATUSError as err:
+            self.assertIsNotNone(expect_error, 'unexpectedly failed')
 
-        num, _ = err.exception.args
-        self.assertEqual(num, expect_error)
+            num, _ = err.args
+            self.assertEqual(num, expect_error)
+        else:
+            self.assertIsNone(expect_error, 'expected to fail')
 
     # Test SamLogon. Authentication should succeed for non-protected accounts,
     # and fail for protected accounts.
index e29d7c0f6c9494937fb531a2534dbe55c643c185..27356cbd589d007c467ba544f9c6b6b178fe8cee 100755 (executable)
@@ -60,6 +60,17 @@ class ProtectedUsersTests(KDCBaseTest):
         self.do_asn1_print = global_asn1_print
         self.do_hexdump = global_hexdump
 
+        samdb = self.get_samdb()
+
+        # Get the old ‘minPwdAge’.
+        minPwdAge = samdb.get_minPwdAge()
+
+        # Reset the ‘minPwdAge’ as it was before.
+        self.addCleanup(samdb.set_minPwdAge, minPwdAge)
+
+        # Set it temporarily to ‘0’.
+        samdb.set_minPwdAge('0')
+
     # Get account credentials for testing.
     def _get_creds(self,
                    protected,
@@ -134,7 +145,7 @@ class ProtectedUsersTests(KDCBaseTest):
 
         self._test_samr_change_password(
             client_creds,
-            expect_error=ntstatus.NT_STATUS_WRONG_PASSWORD)
+            expect_error=None)
 
     def test_samr_change_password_protected(self):
         # Use a non-cached account so that it is not locked out for other