tests/krb5: Add test for presence of NT hash
authorJoseph Sutton <josephsutton@catalyst.net.nz>
Mon, 11 Apr 2022 03:44:09 +0000 (15:44 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Sun, 26 Jun 2022 22:10:29 +0000 (22:10 +0000)
Signed-off-by: Joseph Sutton <josephsutton@catalyst.net.nz>
Reviewed-by: Stefan Metzmacher <metze@samba.org>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/tests/krb5/kdc_base_test.py
python/samba/tests/krb5/nt_hash_tests.py [new file with mode: 0755]
python/samba/tests/usage.py
selftest/knownfail.d/nt-hash-support-gone
source4/selftest/tests.py

index d9efde8273a8302cf4c226bc105f5b4a08645060..794b6f395edec295a01a8b48cc1cd97c95fee8bd 100644 (file)
@@ -537,7 +537,8 @@ class KDCBaseTest(RawKerberosTest):
         req.extended_op = drsuapi.DRSUAPI_EXOP_REPL_SECRET
 
         attids = [drsuapi.DRSUAPI_ATTID_supplementalCredentials,
-                  drsuapi.DRSUAPI_ATTID_unicodePwd]
+                  drsuapi.DRSUAPI_ATTID_unicodePwd,
+                  drsuapi.DRSUAPI_ATTID_ntPwdHistory]
 
         partial_attribute_set = drsuapi.DsPartialAttributeSet()
         partial_attribute_set.version = 1
@@ -596,8 +597,9 @@ class KDCBaseTest(RawKerberosTest):
                                 keys[keytype] = key.value.hex()
             elif attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
                 net_ctx.replicate_decrypt(bind, attr, rid)
-                pwd = attr.value_ctr.values[0].blob
-                keys[kcrypto.Enctype.RC4] = pwd.hex()
+                if attr.value_ctr.num_values > 0:
+                    pwd = attr.value_ctr.values[0].blob
+                    keys[kcrypto.Enctype.RC4] = pwd.hex()
 
         if expected_etypes is None:
             expected_etypes = self.get_default_enctypes()
diff --git a/python/samba/tests/krb5/nt_hash_tests.py b/python/samba/tests/krb5/nt_hash_tests.py
new file mode 100755 (executable)
index 0000000..e64a874
--- /dev/null
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+# Unix SMB/CIFS implementation.
+# Copyright (C) Stefan Metzmacher 2020
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+import os
+import sys
+
+import ldb
+
+from samba import generate_random_password, net
+from samba.dcerpc import drsuapi, misc
+
+from samba.tests.krb5.kdc_base_test import KDCBaseTest
+
+sys.path.insert(0, 'bin/python')
+os.environ['PYTHONUNBUFFERED'] = '1'
+
+global_asn1_print = False
+global_hexdump = False
+
+
+class NtHashTests(KDCBaseTest):
+
+    def setUp(self):
+        super().setUp()
+        self.do_asn1_print = global_asn1_print
+        self.do_hexdump = global_hexdump
+
+    def _check_nt_hash(self, dn, history_len):
+        expect_nt_hash = bool(int(os.environ.get('EXPECT_NT_HASH', '1')))
+
+        samdb = self.get_samdb()
+        admin_creds = self.get_admin_creds()
+
+        bind, identifier, attributes = self.get_secrets(
+            samdb,
+            dn,
+            destination_dsa_guid=misc.GUID(samdb.get_ntds_GUID()),
+            source_dsa_invocation_id=misc.GUID())
+
+        rid = identifier.sid.split()[1]
+
+        net_ctx = net.Net(admin_creds)
+
+        def num_hashes(attr):
+            if attr.value_ctr.values is None:
+                return 0
+
+            net_ctx.replicate_decrypt(bind, attr, rid)
+
+            length = sum(len(value.blob) for value in attr.value_ctr.values)
+            self.assertEqual(0, length & 0xf)
+            return length // 16
+
+        def is_unicodePwd(attr):
+            return attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd
+
+        def is_ntPwdHistory(attr):
+            return attr.attid == drsuapi.DRSUAPI_ATTID_ntPwdHistory
+
+        unicode_pwd_count = sum(attr.value_ctr.num_values
+                                for attr in filter(is_unicodePwd, attributes))
+
+        nt_history_count = sum(num_hashes(attr)
+                               for attr in filter(is_ntPwdHistory, attributes))
+
+        if expect_nt_hash:
+            self.assertEqual(1, unicode_pwd_count,
+                             'expected to find NT hash')
+        else:
+            self.assertEqual(0, unicode_pwd_count,
+                             'got unexpected NT hash')
+
+        if expect_nt_hash:
+            self.assertEqual(history_len, nt_history_count,
+                             'expected to find NT password history')
+        else:
+            self.assertEqual(0, nt_history_count,
+                             'got unexpected NT password history')
+
+    # Test that the NT hash and its history is not generated or stored for an
+    # account when we disable NTLM authentication.
+    def test_nt_hash(self):
+        samdb = self.get_samdb()
+        user_name = self.get_new_username()
+
+        client_creds, client_dn = self.create_account(
+            samdb, user_name,
+            account_type=KDCBaseTest.AccountType.USER)
+
+        self._check_nt_hash(client_dn, history_len=1)
+
+        # Change the password and check that the NT hash is still not present.
+
+        # 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')
+
+        old_utf16pw = f'"{client_creds.get_password()}"'.encode('utf-16-le')
+
+        history_len = 3
+        for _ in range(history_len - 1):
+            password = generate_random_password(32, 32)
+            utf16pw = f'"{password}"'.encode('utf-16-le')
+
+            msg = ldb.Message(ldb.Dn(samdb, client_dn))
+            msg['0'] = ldb.MessageElement(old_utf16pw,
+                                          ldb.FLAG_MOD_DELETE,
+                                          'unicodePwd')
+            msg['1'] = ldb.MessageElement(utf16pw,
+                                          ldb.FLAG_MOD_ADD,
+                                          'unicodePwd')
+            samdb.modify(msg)
+
+            old_utf16pw = utf16pw
+
+        self._check_nt_hash(client_dn, history_len)
+
+
+if __name__ == '__main__':
+    global_asn1_print = False
+    global_hexdump = False
+    import unittest
+    unittest.main()
index f8aed2c67dd057585e15ffafaa7cf2e9f6bed34d..9297247152a378ff9e8159cacbec4de01e9b612e 100644 (file)
@@ -111,6 +111,7 @@ EXCLUDE_USAGE = {
     'python/samba/tests/krb5/test_idmap_nss.py',
     'python/samba/tests/krb5/pac_align_tests.py',
     'python/samba/tests/krb5/protected_users_tests.py',
+    'python/samba/tests/krb5/nt_hash_tests.py',
 }
 
 EXCLUDE_HELP = {
index 94672c402cbc2c81362e7e401a6f49065ff98dfe..1192a6e408f9d7f0e048997651442585a3226bb4 100644 (file)
@@ -1,3 +1,4 @@
+^samba.tests.krb5.nt_hash_tests.samba.tests.krb5.nt_hash_tests.NtHashTests.test_nt_hash.ad_dc_no_ntlm:local
 ^samba.tests.samba_tool.user.samba.tests.samba_tool.user.UserCmdTestCase.test_setpassword.ad_dc_no_ntlm:local
 ^samba4.ldap.login_basics.python.ad_dc_no_ntlm..__main__.BasicUserAuthTests.test_login_basics_ntlm.ad_dc_no_ntlm
 ^samba4.ldap.passwords.python.fl2003dc..__main__.PasswordTests.test_old_password_attempt_reuse.fl2003dc
index 32de26a9d23af9c63275a02063ee3dbe2fa32694..3679200d296e85397b21707ebf76c6037b604ca7 100755 (executable)
@@ -1693,6 +1693,15 @@ planoldpythontestsuite(
     'ad_dc:local',
     'samba.tests.krb5.protected_users_tests',
     environ=krb5_environ)
+for env, nt_hash in [("ad_dc:local", True),
+                     ("ad_dc_no_ntlm:local", False)]:
+    planoldpythontestsuite(
+        env,
+        'samba.tests.krb5.nt_hash_tests',
+        environ={
+            **krb5_environ,
+            'EXPECT_NT_HASH': int(nt_hash),
+    })
 
 for env in [
         'vampire_dc',