torture-samr: Add testing of account lockout and password change behaviour
authorAndrew Bartlett <abartlet@samba.org>
Thu, 31 Oct 2013 03:57:10 +0000 (16:57 +1300)
committerStefan Metzmacher <metze@samba.org>
Wed, 2 Apr 2014 17:30:59 +0000 (19:30 +0200)
This is the regression test to avoid a repeat of CVE-2013-4496

This includes confirming that badPwdCount is updated on login, not just on first failure

However the badPwdCount is not updated if the account is disabled

Note: that samr_QueryUserInfo return the effective bad_password_count in level
5, 16 and 21, while it returns the raw value in level 3.

(Sadly the s3 code does not do this correctly, so a knownfail is added)

Change-Id: I4fd8ac5c3b1357e7a98386756dac2a43eb778ecf
Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Signed-off-by: Stefan Metzmacher <metze@samba.org>
Autobuild-User(master): Stefan Metzmacher <metze@samba.org>
Autobuild-Date(master): Wed Apr  2 19:30:59 CEST 2014 on sn-devel-104

selftest/knownfail
source4/torture/rpc/samr.c

index d229d12fa4e5afa79d70fb29362b343071f9458d..35dba207182a29059673b4ff6e5674627dbe6cd5 100644 (file)
 ^samba3.smb2.compound.interim2 # wrong return code (STATUS_CANCELLED)
 ^samba3.raw.session.*reauth2 # maybe fix this?
 ^samba3.rpc.samr.passwords.badpwdcount.samr.badPwdCount\(s3dc\) # We fail this test currently
+^samba3.rpc.samr.passwords.lockout.*\(s3dc\)$ # We fail this test currently
 ^samba3.rpc.spoolss.printer.addprinter.driver_info_winreg # knownfail or flapping?
 ^samba3.rpc.spoolss.printer.addprinterex.driver_info_winreg # knownfail or flapping?
 ^samba3.rpc.spoolss.printer.*.publish_toggle\(.*\)$ # needs spoolss AD member env
index ff96dc5242875bd36749b4690dc9b21d0707e0ed..293f335363ae0b642910333dc858e76ef7b110b9 100644 (file)
@@ -2298,6 +2298,84 @@ static bool test_ChangePasswordUser2(struct dcerpc_pipe *p, struct torture_conte
 }
 
 
+static bool test_ChangePasswordUser2_ntstatus(struct dcerpc_pipe *p, struct torture_context *tctx,
+                                             const char *acct_name,
+                                             const char *password, NTSTATUS status)
+{
+       struct samr_ChangePasswordUser2 r;
+       bool ret = true;
+       struct lsa_String server, account;
+       struct samr_CryptPassword nt_pass, lm_pass;
+       struct samr_Password nt_verifier, lm_verifier;
+       const char *oldpass;
+       struct dcerpc_binding_handle *b = p->binding_handle;
+       uint8_t old_nt_hash[16], new_nt_hash[16];
+       uint8_t old_lm_hash[16], new_lm_hash[16];
+
+       struct samr_GetDomPwInfo dom_pw_info;
+       struct samr_PwInfo info;
+
+       struct lsa_String domain_name;
+       char *newpass;
+       int policy_min_pw_len = 0;
+
+       domain_name.string = "";
+       dom_pw_info.in.domain_name = &domain_name;
+       dom_pw_info.out.info = &info;
+
+       torture_comment(tctx, "Testing ChangePasswordUser2 on %s\n", acct_name);
+
+       oldpass = password;
+
+       torture_assert_ntstatus_ok(tctx, dcerpc_samr_GetDomPwInfo_r(b, tctx, &dom_pw_info),
+                                  "GetDomPwInfo failed");
+       if (NT_STATUS_IS_OK(dom_pw_info.out.result)) {
+               policy_min_pw_len = dom_pw_info.out.info->min_password_length;
+       }
+
+       newpass = samr_rand_pass(tctx, policy_min_pw_len);
+
+       server.string = talloc_asprintf(tctx, "\\\\%s", dcerpc_server_name(p));
+       init_lsa_String(&account, acct_name);
+
+       E_md4hash(oldpass, old_nt_hash);
+       E_md4hash(newpass, new_nt_hash);
+
+       E_deshash(oldpass, old_lm_hash);
+       E_deshash(newpass, new_lm_hash);
+
+       encode_pw_buffer(lm_pass.data, newpass, STR_ASCII|STR_TERMINATE);
+       arcfour_crypt(lm_pass.data, old_lm_hash, 516);
+       E_old_pw_hash(new_nt_hash, old_lm_hash, lm_verifier.hash);
+
+       encode_pw_buffer(nt_pass.data, newpass, STR_UNICODE);
+       arcfour_crypt(nt_pass.data, old_nt_hash, 516);
+       E_old_pw_hash(new_nt_hash, old_nt_hash, nt_verifier.hash);
+
+       r.in.server = &server;
+       r.in.account = &account;
+       r.in.nt_password = &nt_pass;
+       r.in.nt_verifier = &nt_verifier;
+       r.in.lm_change = 1;
+       r.in.lm_password = &lm_pass;
+       r.in.lm_verifier = &lm_verifier;
+
+       torture_assert_ntstatus_ok(tctx, dcerpc_samr_ChangePasswordUser2_r(b, tctx, &r),
+               "ChangePasswordUser2 failed");
+       torture_comment(tctx, "(%s:%s) old_password[%s] new_password[%s] status[%s]\n",
+                       __location__, __FUNCTION__,
+                       oldpass, newpass, nt_errstr(r.out.result));
+
+       if (NT_STATUS_EQUAL(r.out.result, NT_STATUS_PASSWORD_RESTRICTION)) {
+               torture_comment(tctx, "ChangePasswordUser2 returned: %s perhaps min password age? (not fatal)\n", nt_errstr(r.out.result));
+       } else {
+               torture_assert_ntstatus_equal(tctx, r.out.result, status, "ChangePasswordUser2 returned unexpected value");
+       }
+
+       return true;
+}
+
+
 bool test_ChangePasswordUser3(struct dcerpc_pipe *p, struct torture_context *tctx,
                              const char *account_string,
                              int policy_min_pw_len,
@@ -4066,14 +4144,16 @@ static bool test_Password_badpwdcount_wrap(struct dcerpc_pipe *p,
        return ret;
 }
 
-static bool test_QueryUserInfo_acct_flags(struct dcerpc_binding_handle *b,
-                                         struct torture_context *tctx,
-                                         struct policy_handle *domain_handle,
-                                         const char *acct_name,
-                                         uint32_t *acct_flags)
+static bool test_QueryUserInfo_lockout(struct dcerpc_binding_handle *b,
+                                      struct torture_context *tctx,
+                                      struct policy_handle *domain_handle,
+                                      const char *acct_name,
+                                      uint16_t raw_bad_password_count,
+                                      uint16_t effective_bad_password_count,
+                                      uint32_t effective_acb_lockout)
 {
        struct policy_handle user_handle;
-       union samr_UserInfo *info;
+       union samr_UserInfo *i;
        struct samr_QueryUserInfo r;
 
        NTSTATUS status = test_OpenUser_byname(b, tctx, domain_handle, acct_name, &user_handle);
@@ -4082,19 +4162,73 @@ static bool test_QueryUserInfo_acct_flags(struct dcerpc_binding_handle *b,
        }
 
        r.in.user_handle = &user_handle;
-       r.in.level = 16;
-       r.out.info = &info;
-
+       r.in.level = 3;
+       r.out.info = &i;
        torture_comment(tctx, "Testing QueryUserInfo level %d", r.in.level);
+       torture_assert_ntstatus_ok(tctx, dcerpc_samr_QueryUserInfo_r(b, tctx, &r),
+               "failed to query userinfo");
+       torture_assert_ntstatus_ok(tctx, r.out.result,
+               "failed to query userinfo");
+       torture_comment(tctx, "  (acct_flags: 0x%08x) (raw_bad_pwd_count: %u)\n",
+                       i->info3.acct_flags, i->info3.bad_password_count);
+       torture_assert_int_equal(tctx, i->info3.bad_password_count,
+                                raw_bad_password_count,
+                                "raw badpwdcount");
+       torture_assert_int_equal(tctx, i->info3.acct_flags & ACB_AUTOLOCK,
+                                effective_acb_lockout,
+                                "effective acb_lockout");
+       TALLOC_FREE(i);
 
+       r.in.user_handle = &user_handle;
+       r.in.level = 5;
+       r.out.info = &i;
+       torture_comment(tctx, "Testing QueryUserInfo level %d", r.in.level);
        torture_assert_ntstatus_ok(tctx, dcerpc_samr_QueryUserInfo_r(b, tctx, &r),
                "failed to query userinfo");
        torture_assert_ntstatus_ok(tctx, r.out.result,
                "failed to query userinfo");
+       torture_comment(tctx, "  (acct_flags: 0x%08x) (effective_bad_pwd_count: %u)\n",
+                       i->info5.acct_flags, i->info5.bad_password_count);
+       torture_assert_int_equal(tctx, i->info5.bad_password_count,
+                                effective_bad_password_count,
+                                "effective badpwdcount");
+       torture_assert_int_equal(tctx, i->info5.acct_flags & ACB_AUTOLOCK,
+                                effective_acb_lockout,
+                                "effective acb_lockout");
+       TALLOC_FREE(i);
 
-       *acct_flags = info->info16.acct_flags;
+       r.in.user_handle = &user_handle;
+       r.in.level = 16;
+       r.out.info = &i;
+       torture_comment(tctx, "Testing QueryUserInfo level %d", r.in.level);
+       torture_assert_ntstatus_ok(tctx, dcerpc_samr_QueryUserInfo_r(b, tctx, &r),
+               "failed to query userinfo");
+       torture_assert_ntstatus_ok(tctx, r.out.result,
+               "failed to query userinfo");
+       torture_comment(tctx, "  (acct_flags: 0x%08x)\n",
+                       i->info16.acct_flags);
+       torture_assert_int_equal(tctx, i->info16.acct_flags & ACB_AUTOLOCK,
+                                effective_acb_lockout,
+                                "effective acb_lockout");
+       TALLOC_FREE(i);
 
-       torture_comment(tctx, "  (acct_flags: 0x%08x)\n", *acct_flags);
+       r.in.user_handle = &user_handle;
+       r.in.level = 21;
+       r.out.info = &i;
+       torture_comment(tctx, "Testing QueryUserInfo level %d", r.in.level);
+       torture_assert_ntstatus_ok(tctx, dcerpc_samr_QueryUserInfo_r(b, tctx, &r),
+               "failed to query userinfo");
+       torture_assert_ntstatus_ok(tctx, r.out.result,
+               "failed to query userinfo");
+       torture_comment(tctx, "  (acct_flags: 0x%08x) (effective_bad_pwd_count: %u)\n",
+                       i->info21.acct_flags, i->info21.bad_password_count);
+       torture_assert_int_equal(tctx, i->info21.bad_password_count,
+                                effective_bad_password_count,
+                                "effective badpwdcount");
+       torture_assert_int_equal(tctx, i->info21.acct_flags & ACB_AUTOLOCK,
+                                effective_acb_lockout,
+                                "effective acb_lockout");
+       TALLOC_FREE(i);
 
        if (!test_samr_handle_Close(b, tctx, &user_handle)) {
                return false;
@@ -4121,7 +4255,6 @@ static bool test_Password_lockout(struct dcerpc_pipe *p,
                                  struct samr_DomInfo12 *info12)
 {
        union samr_DomainInfo info;
-       uint32_t badpwdcount;
        uint64_t lockout_threshold = 1;
        uint32_t lockout_seconds = 5;
        uint64_t delta_time_factor = 10 * 1000 * 1000;
@@ -4209,9 +4342,9 @@ static bool test_Password_lockout(struct dcerpc_pipe *p,
        }
 
        torture_assert(tctx,
-               test_QueryUserInfo_badpwdcount(b, tctx, user_handle, &badpwdcount), "");
-       torture_assert_int_equal(tctx, badpwdcount, 0, "expected badpwdcount to be 0");
-
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       0, 0, 0),
+               "expected account to not be locked");
 
        /* test with wrong password ==> lockout */
 
@@ -4221,16 +4354,14 @@ static bool test_Password_lockout(struct dcerpc_pipe *p,
                torture_fail(tctx, "succeeded to authenticate with wrong password");
        }
 
+       /*
+        * curiously, windows does _not_ return fresh values of
+        * effective bad_password_count and ACB_AUTOLOCK.
+        */
        torture_assert(tctx,
-               test_QueryUserInfo_badpwdcount(b, tctx, user_handle, &badpwdcount), "");
-       torture_assert_int_equal(tctx, badpwdcount, 1, "expected badpwdcount to be 1");
-
-       /* curiously, windows does _not_ set the autlock flag unless you re-open the user */
-       torture_assert(tctx,
-                      test_QueryUserInfo_acct_flags(b, tctx, domain_handle, acct_name, &acct_flags), "");
-       torture_assert_int_equal(tctx, acct_flags & ACB_AUTOLOCK, ACB_AUTOLOCK,
-                                "expected account to be locked");
-
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, ACB_AUTOLOCK),
+               "expected account to not be locked");
 
        /* test with good password */
 
@@ -4243,15 +4374,30 @@ static bool test_Password_lockout(struct dcerpc_pipe *p,
 
        /* bad pwd count should not get updated */
        torture_assert(tctx,
-               test_QueryUserInfo_badpwdcount(b, tctx, user_handle, &badpwdcount), "");
-       torture_assert_int_equal(tctx, badpwdcount, 1, "expected badpwdcount to be 1");
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, ACB_AUTOLOCK),
+               "expected account to be locked");
+
+       torture_assert(tctx,
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, *password,
+                                                        NT_STATUS_ACCOUNT_LOCKED_OUT),
+                      "got wrong status from ChangePasswordUser2");
 
-       /* curiously, windows does _not_ set the autlock flag unless you re-open the user */
+       /* bad pwd count should not get updated */
        torture_assert(tctx,
-                      test_QueryUserInfo_acct_flags(b, tctx, domain_handle, acct_name, &acct_flags), "");
-       torture_assert_int_equal(tctx, acct_flags & ACB_AUTOLOCK, ACB_AUTOLOCK,
-                                "expected account to be locked");
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, ACB_AUTOLOCK),
+               "expected account to be locked");
 
+       torture_assert(tctx,
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, "random_crap", NT_STATUS_ACCOUNT_LOCKED_OUT),
+                      "got wrong status from ChangePasswordUser2");
+
+       /* bad pwd count should not get updated */
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, ACB_AUTOLOCK),
+               "expected account to be locked");
 
        /* with bad password */
 
@@ -4264,21 +4410,76 @@ static bool test_Password_lockout(struct dcerpc_pipe *p,
 
        /* bad pwd count should not get updated */
        torture_assert(tctx,
-               test_QueryUserInfo_badpwdcount(b, tctx, user_handle, &badpwdcount), "");
-       torture_assert_int_equal(tctx, badpwdcount, 1, "expected badpwdcount to be 1");
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, ACB_AUTOLOCK),
+               "expected account to be locked");
+
+       /* let lockout duration expire ==> unlock */
+
+       torture_comment(tctx, "let lockout duration expire...\n");
+       sleep(lockout_seconds + 1);
+
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 0, 0),
+               "expected account to not be locked");
+
+       if (!test_SamLogon_with_creds(tctx, np, machine_credentials, acct_name,
+                                    *password,
+                                    expected_success_status, interactive))
+       {
+               torture_fail(tctx, "failed to authenticate after lockout expired");
+       }
+
+       if (NT_STATUS_IS_OK(expected_success_status)) {
+               torture_assert(tctx,
+                       test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                               0, 0, 0),
+                       "expected account to not be locked");
+       } else {
+               torture_assert(tctx,
+                       test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                               1, 0, 0),
+                       "expected account to not be locked");
+       }
 
-       /* curiously, windows does _not_ set the autlock flag untill you re-open the user */
        torture_assert(tctx,
-                      test_QueryUserInfo_acct_flags(b, tctx, domain_handle, acct_name, &acct_flags), "");
-       torture_assert_int_equal(tctx, acct_flags & ACB_AUTOLOCK, ACB_AUTOLOCK,
-                                "expected account to show ACB_AUTOLOCK");
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, "random_crap", NT_STATUS_WRONG_PASSWORD),
+                      "got wrong status from ChangePasswordUser2");
 
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, ACB_AUTOLOCK),
+               "expected account to be locked");
+
+       torture_assert(tctx,
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, *password, NT_STATUS_ACCOUNT_LOCKED_OUT),
+                      "got wrong status from ChangePasswordUser2");
+
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, ACB_AUTOLOCK),
+               "expected account to be locked");
+
+       torture_assert(tctx,
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, "random_crap", NT_STATUS_ACCOUNT_LOCKED_OUT),
+                      "got wrong status from ChangePasswordUser2");
+
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, ACB_AUTOLOCK),
+               "expected account to be locked");
 
        /* let lockout duration expire ==> unlock */
 
        torture_comment(tctx, "let lockout duration expire...\n");
        sleep(lockout_seconds + 1);
 
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 0, 0),
+               "expected account to not be locked");
+
        if (!test_SamLogon_with_creds(tctx, np, machine_credentials, acct_name,
                                     *password,
                                     expected_success_status, interactive))
@@ -4286,10 +4487,116 @@ static bool test_Password_lockout(struct dcerpc_pipe *p,
                torture_fail(tctx, "failed to authenticate after lockout expired");
        }
 
+       if (NT_STATUS_IS_OK(expected_success_status)) {
+               torture_assert(tctx,
+                       test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                               0, 0, 0),
+                       "expected account to not be locked");
+       } else {
+               torture_assert(tctx,
+                       test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                               1, 0, 0),
+                       "expected account to not be locked");
+       }
+
+       /* Testing ChangePasswordUser behaviour with 3 attempts */
+       info.info12.lockout_threshold = 3;
+
+       torture_assert(tctx,
+                      test_SetDomainInfo(b, tctx, domain_handle,
+                                         DomainLockoutInformation, &info),
+                      "failed to set lockout threshold to 3");
+
+       if (NT_STATUS_IS_OK(expected_success_status)) {
+               torture_assert(tctx,
+                       test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                               0, 0, 0),
+                       "expected account to not be locked");
+       } else {
+               torture_assert(tctx,
+                       test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                               1, 0, 0),
+                       "expected account to not be locked");
+       }
+
+       torture_assert(tctx,
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, "random_crap", NT_STATUS_WRONG_PASSWORD),
+                      "got wrong status from ChangePasswordUser2");
+
+       /* bad pwd count will get updated */
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       1, 1, 0),
+               "expected account to not be locked");
+
+       torture_assert(tctx,
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, "random_crap", NT_STATUS_WRONG_PASSWORD),
+                      "got wrong status from ChangePasswordUser2");
+
+       /* bad pwd count will get updated */
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       2, 2, 0),
+               "expected account to not be locked");
+
+       torture_assert(tctx,
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, "random_crap", NT_STATUS_WRONG_PASSWORD),
+                      "got wrong status from ChangePasswordUser2");
+
+       /* bad pwd count should get updated */
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       3, 3, ACB_AUTOLOCK),
+               "expected account to be locked");
+
        torture_assert(tctx,
-                      test_QueryUserInfo_acct_flags(b, tctx, domain_handle, acct_name, &acct_flags), "");
-       torture_assert_int_equal(tctx, acct_flags & ACB_AUTOLOCK, 0,
-                                "expected account not to be locked");
+                      test_ChangePasswordUser2_ntstatus(p, tctx, acct_name, *password, NT_STATUS_ACCOUNT_LOCKED_OUT),
+                      "got wrong status from ChangePasswordUser2");
+
+       /* bad pwd count should not get updated */
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       3, 3, ACB_AUTOLOCK),
+               "expected account to be locked");
+
+       /* let lockout duration expire ==> unlock */
+
+       torture_comment(tctx, "let lockout duration expire...\n");
+       sleep(lockout_seconds + 1);
+
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       3, 0, 0),
+               "expected account to not be locked");
+
+       torture_assert(tctx,
+                      test_ChangePasswordUser2(p, tctx, acct_name, password, NULL, false),
+                      "got wrong status from ChangePasswordUser2");
+
+       torture_assert(tctx,
+               test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                       3, 0, 0),
+               "expected account to not be locked");
+
+       /* Used to reset the badPwdCount for the other tests */
+       if (!test_SamLogon_with_creds(tctx, np, machine_credentials, acct_name,
+                                     *password,
+                                     expected_success_status, interactive))
+       {
+               torture_fail(tctx, "failed to authenticate after lockout expired");
+       }
+
+       if (NT_STATUS_IS_OK(expected_success_status)) {
+               torture_assert(tctx,
+                       test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                               0, 0, 0),
+                       "expected account to not be locked");
+       } else {
+               torture_assert(tctx,
+                       test_QueryUserInfo_lockout(b, tctx, domain_handle, acct_name,
+                               3, 0, 0),
+                       "expected account to not be locked");
+       }
 
        return true;
 }