From 68c57d9f78dd511d7238fbc6a479f5db928d5eee Mon Sep 17 00:00:00 2001 From: Joseph Sutton Date: Mon, 11 Apr 2022 15:44:09 +1200 Subject: tests/krb5: Add test for presence of NT hash Signed-off-by: Joseph Sutton Reviewed-by: Stefan Metzmacher Reviewed-by: Andrew Bartlett --- python/samba/tests/krb5/kdc_base_test.py | 8 +- python/samba/tests/krb5/nt_hash_tests.py | 143 +++++++++++++++++++++++++++++++ python/samba/tests/usage.py | 1 + 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100755 python/samba/tests/krb5/nt_hash_tests.py (limited to 'python') diff --git a/python/samba/tests/krb5/kdc_base_test.py b/python/samba/tests/krb5/kdc_base_test.py index d9efde8273a..794b6f395ed 100644 --- a/python/samba/tests/krb5/kdc_base_test.py +++ b/python/samba/tests/krb5/kdc_base_test.py @@ -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 index 00000000000..e64a874b080 --- /dev/null +++ b/python/samba/tests/krb5/nt_hash_tests.py @@ -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 . +# + +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() diff --git a/python/samba/tests/usage.py b/python/samba/tests/usage.py index f8aed2c67dd..9297247152a 100644 --- a/python/samba/tests/usage.py +++ b/python/samba/tests/usage.py @@ -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 = { -- cgit v1.2.3