]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
tests/password_lockout: Test NTLM and SAMR password changes with Protected Users
authorJoseph Sutton <josephsutton@catalyst.net.nz>
Wed, 9 Feb 2022 00:50:10 +0000 (13:50 +1300)
committerStefan Metzmacher <metze@samba.org>
Fri, 18 Mar 2022 11:55:30 +0000 (11:55 +0000)
Test that NTLM and SAMR password changes cannot be used for Protected
Users, and that lockouts are not triggered for attempting to use them.

Signed-off-by: Joseph Sutton <josephsutton@catalyst.net.nz>
Reviewed-by: Stefan Metzmacher <metze@samba.org>
selftest/knownfail.d/protected_users [new file with mode: 0644]
source4/dsdb/tests/python/password_lockout.py

diff --git a/selftest/knownfail.d/protected_users b/selftest/knownfail.d/protected_users
new file mode 100644 (file)
index 0000000..c037038
--- /dev/null
@@ -0,0 +1,3 @@
+^samba4.ldap.password_lockout.python\(ad_dc_slowtests\).__main__.PasswordTestsWithoutSleep.test_ntlm_lockout_protected.ad_dc_slowtests
+^samba4.ldap.password_lockout.python\(ad_dc_slowtests\).__main__.PasswordTestsWithoutSleep.test_samr_change_password_protected.ad_dc_slowtests
+^samba4.ldap.password_lockout.python\(ad_dc_slowtests\).__main__.PasswordTestsWithoutSleep.test_samr_set_password_protected.ad_dc_slowtests
index 9aa23afd774ce60d8df9c0567252ed725cf6cc80..a1a0ae0e8647df27bb6e5a29d1a4c380b6f653af 100755 (executable)
@@ -775,6 +775,283 @@ userPassword: thatsAcomplPASS2XYZ
         self._test_samr_password_change(self.lockout1ntlm_creds,
                                         other_creds=self.lockout2ntlm_creds)
 
+    def test_ntlm_lockout_protected(self):
+        creds = self.lockout1ntlm_creds
+        self.assertEqual(DONT_USE_KERBEROS, creds.get_kerberos_state())
+
+        # Work out the initial account values for this user.
+        username = creds.get_username()
+        userdn = f'cn={username},cn=users,{self.base_dn}'
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=('greater', 0),
+                                  badPwdCountOnly=True)
+        badPasswordTime = int(res[0]['badPasswordTime'][0])
+        logonCount = int(res[0]['logonCount'][0])
+        lastLogon = int(res[0]['lastLogon'][0])
+        lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+        # Add the user to the Protected Users group.
+
+        # Search for the Protected Users group.
+        group_dn = Dn(self.ldb,
+                      f'<SID={self.ldb.get_domain_sid()}-'
+                      f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+        try:
+            group_res = self.ldb.search(base=group_dn,
+                                        scope=SCOPE_BASE,
+                                        attrs=['member'])
+        except LdbError as err:
+            self.fail(err)
+
+        orig_msg = group_res[0]
+
+        # Add the user to the list of members.
+        members = list(orig_msg.get('member', ()))
+        self.assertNotIn(userdn, members, 'account already in Protected Users')
+        members.append(userdn)
+
+        m = Message(group_dn)
+        m['member'] = MessageElement(members,
+                                     FLAG_MOD_REPLACE,
+                                     'member')
+        cleanup = self.ldb.msg_diff(m, orig_msg)
+        self.ldb.modify(m)
+
+        password = creds.get_password()
+        creds.set_password('wrong_password')
+
+        lockout_threshold = 5
+
+        lp = self.get_loadparm()
+        server = f'ldap://{self.ldb.host_dns_name()}'
+
+        for _ in range(lockout_threshold):
+            with self.assertRaises(LdbError) as err:
+                SamDB(url=server,
+                      credentials=creds,
+                      lp=lp)
+
+            num, _ = err.exception.args
+            self.assertEqual(ERR_INVALID_CREDENTIALS, num)
+
+            res = self._check_account(
+                userdn,
+                badPwdCount=0,
+                badPasswordTime=badPasswordTime,
+                logonCount=logonCount,
+                lastLogon=lastLogon,
+                lastLogonTimestamp=lastLogonTimestamp,
+                lockoutTime=None,
+                userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                msDSUserAccountControlComputed=0)
+
+        # The user should not be locked out.
+        self.assertNotIn('lockoutTime', res[0],
+                         'account unexpectedly locked out')
+
+        # Move the account out of 'Protected Users'.
+        self.ldb.modify(cleanup)
+
+        # The account should not be locked out.
+        creds.set_password(password)
+
+        try:
+            SamDB(url=server,
+                  credentials=creds,
+                  lp=lp)
+        except LdbError:
+            self.fail('account unexpectedly locked out')
+
+    def test_samr_change_password_protected(self):
+        """Tests the SAMR password change method for Protected Users"""
+
+        creds = self.lockout1ntlm_creds
+        other_creds = self.lockout2ntlm_creds
+        lockout_threshold = 5
+
+        # Create a connection for SAMR using another user's credentials.
+        lp = self.get_loadparm()
+        net = Net(other_creds, lp, server=self.host)
+
+        # Work out the initial account values for this user.
+        username = creds.get_username()
+        userdn = f'cn={username},cn=users,{self.base_dn}'
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=('greater', 0),
+                                  badPwdCountOnly=True)
+        badPasswordTime = int(res[0]['badPasswordTime'][0])
+        logonCount = int(res[0]['logonCount'][0])
+        lastLogon = int(res[0]['lastLogon'][0])
+        lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+        # prove we can change the user password (using the correct password)
+        new_password = 'thatsAcomplPASS1'
+        net.change_password(newpassword=new_password,
+                            username=username,
+                            oldpassword=creds.get_password())
+        creds.set_password(new_password)
+
+        # Add the user to the Protected Users group.
+
+        # Search for the Protected Users group.
+        group_dn = Dn(self.ldb,
+                      f'<SID={self.ldb.get_domain_sid()}-'
+                      f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+        try:
+            group_res = self.ldb.search(base=group_dn,
+                                        scope=SCOPE_BASE,
+                                        attrs=['member'])
+        except LdbError as err:
+            self.fail(err)
+
+        orig_msg = group_res[0]
+
+        # Add the user to the list of members.
+        members = list(orig_msg.get('member', ()))
+        self.assertNotIn(userdn, members, 'account already in Protected Users')
+        members.append(userdn)
+
+        m = Message(group_dn)
+        m['member'] = MessageElement(members,
+                                     FLAG_MOD_REPLACE,
+                                     'member')
+        self.ldb.modify(m)
+
+        # Try entering the correct password 'x' times in a row, which should
+        # fail, but not lock the user out.
+        new_password = 'thatsAcomplPASS2'
+        for i in range(lockout_threshold):
+            with self.assertRaises(
+                    NTSTATUSError,
+                    msg='Invalid SAMR change_password accepted') as err:
+                print(f'Trying correct password, attempt #{i}')
+                net.change_password(newpassword=new_password,
+                                    username=username,
+                                    oldpassword=creds.get_password())
+
+            enum = ctypes.c_uint32(err.exception.args[0]).value
+            self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_RESTRICTION)
+
+            res = self._check_account(
+                userdn,
+                badPwdCount=0,
+                badPasswordTime=badPasswordTime,
+                logonCount=logonCount,
+                lastLogon=lastLogon,
+                lastLogonTimestamp=lastLogonTimestamp,
+                lockoutTime=None,
+                userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                msDSUserAccountControlComputed=0)
+
+        # The user should not be locked out.
+        self.assertNotIn('lockoutTime', res[0])
+
+        # Ensure that the password can still be changed via LDAP.
+        self.ldb.modify_ldif(f'''
+dn: {userdn}
+changetype: modify
+delete: userPassword
+userPassword: {creds.get_password()}
+add: userPassword
+userPassword: {new_password}
+''')
+
+    def test_samr_set_password_protected(self):
+        """Tests the SAMR password set method for Protected Users"""
+
+        creds = self.lockout1ntlm_creds
+        lockout_threshold = 5
+
+        # create a connection for SAMR using another user's credentials
+        lp = self.get_loadparm()
+        net = Net(self.global_creds, lp, server=self.host)
+
+        # work out the initial account values for this user
+        username = creds.get_username()
+        userdn = f'cn={username},cn=users,{self.base_dn}'
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=('greater', 0),
+                                  badPwdCountOnly=True)
+        badPasswordTime = int(res[0]['badPasswordTime'][0])
+        logonCount = int(res[0]['logonCount'][0])
+        lastLogon = int(res[0]['lastLogon'][0])
+        lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+        # prove we can change the user password (using the correct password)
+        new_password = 'thatsAcomplPASS1'
+        net.set_password(newpassword=new_password,
+                         account_name=username,
+                         domain_name=creds.get_domain())
+        creds.set_password(new_password)
+
+        # Add the user to the Protected Users group.
+
+        # Search for the Protected Users group.
+        group_dn = Dn(self.ldb,
+                      f'<SID={self.ldb.get_domain_sid()}-'
+                      f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+        try:
+            group_res = self.ldb.search(base=group_dn,
+                                        scope=SCOPE_BASE,
+                                        attrs=['member'])
+        except LdbError as err:
+            self.fail(err)
+
+        orig_msg = group_res[0]
+
+        # Add the user to the list of members.
+        members = list(orig_msg.get('member', ()))
+        self.assertNotIn(userdn, members, 'account already in Protected Users')
+        members.append(userdn)
+
+        m = Message(group_dn)
+        m['member'] = MessageElement(members,
+                                     FLAG_MOD_REPLACE,
+                                     'member')
+        self.ldb.modify(m)
+
+        # Try entering the correct password 'x' times in a row, which should
+        # fail, but not lock the user out.
+        new_password = 'thatsAcomplPASS2'
+        for i in range(lockout_threshold):
+            with self.assertRaises(
+                    NTSTATUSError,
+                    msg='Invalid SAMR set_password accepted') as err:
+                print(f'Trying correct password, attempt #{i}')
+                net.set_password(newpassword=new_password,
+                                 account_name=username,
+                                 domain_name=creds.get_domain())
+
+            enum = ctypes.c_uint32(err.exception.args[0]).value
+            self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_RESTRICTION)
+
+            res = self._check_account(
+                userdn,
+                badPwdCount=0,
+                badPasswordTime=badPasswordTime,
+                logonCount=logonCount,
+                lastLogon=lastLogon,
+                lastLogonTimestamp=lastLogonTimestamp,
+                lockoutTime=None,
+                userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                msDSUserAccountControlComputed=0)
+
+        # The user should not be locked out.
+        self.assertNotIn('lockoutTime', res[0])
+
+        # Ensure that the password can still be changed via LDAP.
+        self.ldb.modify_ldif(f'''
+dn: {userdn}
+changetype: modify
+delete: userPassword
+userPassword: {creds.get_password()}
+add: userPassword
+userPassword: {new_password}
+''')
+
 
 class PasswordTestsWithSleep(PasswordTests):
     def setUp(self):