|
|
b69e47 |
From 95b39e29361812a62f2e038c89a88d717c82794e Mon Sep 17 00:00:00 2001
|
|
|
b69e47 |
From: William Brown <firstyear@redhat.com>
|
|
|
b69e47 |
Date: Mon, 31 Jul 2017 14:13:49 +1000
|
|
|
b69e47 |
Subject: [PATCH] Ticket 49336 - SECURITY: Locked account provides different
|
|
|
b69e47 |
return code
|
|
|
b69e47 |
|
|
|
b69e47 |
Bug Description: The directory server password lockout policy prevents binds
|
|
|
b69e47 |
from operating once a threshold of failed passwords has been met. During
|
|
|
b69e47 |
this lockout, if you bind with a successful password, a different error code
|
|
|
b69e47 |
is returned. This means that an attacker has no ratelimit or penalty during
|
|
|
b69e47 |
an account lock, and can continue to attempt passwords via bruteforce, using
|
|
|
b69e47 |
the change in return code to ascertain a sucessful password auth.
|
|
|
b69e47 |
|
|
|
b69e47 |
Fix Description: Move the account lock check *before* the password bind
|
|
|
b69e47 |
check. If the account is locked, we do not mind disclosing this as the
|
|
|
b69e47 |
attacker will either ignore it (and will not bind anyway), or they will
|
|
|
b69e47 |
be forced to back off as the attack is not working preventing the
|
|
|
b69e47 |
bruteforce.
|
|
|
b69e47 |
|
|
|
b69e47 |
https://pagure.io/389-ds-base/issue/49336
|
|
|
b69e47 |
|
|
|
b69e47 |
Author: wibrown
|
|
|
b69e47 |
|
|
|
b69e47 |
Review by: tbordaz (Thanks!)
|
|
|
b69e47 |
|
|
|
b69e47 |
Signed-off-by: Mark Reynolds <mreynolds@redhat.com>
|
|
|
b69e47 |
---
|
|
|
b69e47 |
.../suites/password/pwd_lockout_bypass_test.py | 55 ++++++++++++++++++++++
|
|
|
b69e47 |
ldap/servers/slapd/bind.c | 29 ++++++++----
|
|
|
b69e47 |
ldap/servers/slapd/pw_verify.c | 15 +++---
|
|
|
b69e47 |
3 files changed, 84 insertions(+), 15 deletions(-)
|
|
|
b69e47 |
create mode 100644 dirsrvtests/tests/suites/password/pwd_lockout_bypass_test.py
|
|
|
b69e47 |
|
|
|
b69e47 |
diff --git a/dirsrvtests/tests/suites/password/pwd_lockout_bypass_test.py b/dirsrvtests/tests/suites/password/pwd_lockout_bypass_test.py
|
|
|
b69e47 |
new file mode 100644
|
|
|
b69e47 |
index 0000000..e4add72
|
|
|
b69e47 |
--- /dev/null
|
|
|
b69e47 |
+++ b/dirsrvtests/tests/suites/password/pwd_lockout_bypass_test.py
|
|
|
b69e47 |
@@ -0,0 +1,55 @@
|
|
|
b69e47 |
+# --- BEGIN COPYRIGHT BLOCK ---
|
|
|
b69e47 |
+# Copyright (C) 2017 Red Hat, Inc.
|
|
|
b69e47 |
+# All rights reserved.
|
|
|
b69e47 |
+#
|
|
|
b69e47 |
+# License: GPL (version 3 or any later version).
|
|
|
b69e47 |
+# See LICENSE for details.
|
|
|
b69e47 |
+# --- END COPYRIGHT BLOCK ---
|
|
|
b69e47 |
+#
|
|
|
b69e47 |
+import pytest
|
|
|
b69e47 |
+from lib389.tasks import *
|
|
|
b69e47 |
+from lib389.utils import *
|
|
|
b69e47 |
+from lib389.topologies import topology_st
|
|
|
b69e47 |
+from lib389.idm.user import UserAccounts, TEST_USER_PROPERTIES
|
|
|
b69e47 |
+import ldap
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+# The irony of these names is not lost on me.
|
|
|
b69e47 |
+GOOD_PASSWORD = 'password'
|
|
|
b69e47 |
+BAD_PASSWORD = 'aontseunao'
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+logging.getLogger(__name__).setLevel(logging.INFO)
|
|
|
b69e47 |
+log = logging.getLogger(__name__)
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+def test_lockout_bypass(topology_st):
|
|
|
b69e47 |
+ inst = topology_st.standalone
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+ # Configure the lock policy
|
|
|
b69e47 |
+ inst.config.set('passwordMaxFailure', '1')
|
|
|
b69e47 |
+ inst.config.set('passwordLockoutDuration', '99999')
|
|
|
b69e47 |
+ inst.config.set('passwordLockout', 'on')
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+ # Create the account
|
|
|
b69e47 |
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
|
|
|
b69e47 |
+ testuser = users.create(properties=TEST_USER_PROPERTIES)
|
|
|
b69e47 |
+ testuser.set('userPassword', GOOD_PASSWORD)
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+ conn = testuser.bind(GOOD_PASSWORD)
|
|
|
b69e47 |
+ assert conn != None
|
|
|
b69e47 |
+ conn.unbind_s()
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+ # Bind with bad creds twice
|
|
|
b69e47 |
+ # This is the failure.
|
|
|
b69e47 |
+ with pytest.raises(ldap.INVALID_CREDENTIALS):
|
|
|
b69e47 |
+ conn = testuser.bind(BAD_PASSWORD)
|
|
|
b69e47 |
+ # Now we should not be able to ATTEMPT the bind. It doesn't matter that
|
|
|
b69e47 |
+ # we disclose that we have hit the rate limit here, what matters is that
|
|
|
b69e47 |
+ # it exists.
|
|
|
b69e47 |
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
|
|
|
b69e47 |
+ conn = testuser.bind(BAD_PASSWORD)
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+ # now bind with good creds
|
|
|
b69e47 |
+ # Should be error 19 still.
|
|
|
b69e47 |
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
|
|
|
b69e47 |
+ conn = testuser.bind(GOOD_PASSWORD)
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+
|
|
|
b69e47 |
diff --git a/ldap/servers/slapd/bind.c b/ldap/servers/slapd/bind.c
|
|
|
b69e47 |
index 7f4414f..064ace1 100644
|
|
|
b69e47 |
--- a/ldap/servers/slapd/bind.c
|
|
|
b69e47 |
+++ b/ldap/servers/slapd/bind.c
|
|
|
b69e47 |
@@ -662,12 +662,14 @@ do_bind( Slapi_PBlock *pb )
|
|
|
b69e47 |
/* We could be serving multiple database backends. Select the appropriate one */
|
|
|
b69e47 |
/* pw_verify_be_dn will select the backend we need for us. */
|
|
|
b69e47 |
|
|
|
b69e47 |
- if (auto_bind) {
|
|
|
b69e47 |
- /* We have no password material. We should just check who we are binding as. */
|
|
|
b69e47 |
- rc = pw_validate_be_dn(pb, &referral);
|
|
|
b69e47 |
- } else {
|
|
|
b69e47 |
- rc = pw_verify_be_dn(pb, &referral);
|
|
|
b69e47 |
- }
|
|
|
b69e47 |
+ /*
|
|
|
b69e47 |
+ * WARNING: We have to validate *all* other conditions *first* before
|
|
|
b69e47 |
+ * we attempt the bind!
|
|
|
b69e47 |
+ *
|
|
|
b69e47 |
+ * this is because ldbm_bind.c will SEND THE FAILURE.
|
|
|
b69e47 |
+ */
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+ rc = pw_validate_be_dn(pb, &referral);
|
|
|
b69e47 |
|
|
|
b69e47 |
if (rc == SLAPI_BIND_NO_BACKEND) {
|
|
|
b69e47 |
send_nobackend_ldap_result( pb );
|
|
|
b69e47 |
@@ -736,8 +738,18 @@ do_bind( Slapi_PBlock *pb )
|
|
|
b69e47 |
myrc = 0;
|
|
|
b69e47 |
}
|
|
|
b69e47 |
if (!auto_bind) {
|
|
|
b69e47 |
- /*
|
|
|
b69e47 |
- * There could be a race that bind_target_entry was not added
|
|
|
b69e47 |
+ /*
|
|
|
b69e47 |
+ * Okay, we've made it here. FINALLY check if the entry really
|
|
|
b69e47 |
+ * can bind or not. THIS IS THE PASSWORD CHECK.
|
|
|
b69e47 |
+ */
|
|
|
b69e47 |
+ rc = pw_verify_be_dn(pb, &referral);
|
|
|
b69e47 |
+ if (rc != SLAPI_BIND_SUCCESS) {
|
|
|
b69e47 |
+ /* Invalid pass - lets bail ... */
|
|
|
b69e47 |
+ goto bind_failed;
|
|
|
b69e47 |
+ }
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+ /*
|
|
|
b69e47 |
+ * There could be a race that bind_target_entry was not added
|
|
|
b69e47 |
* when bind_target_entry was retrieved before be_bind, but it
|
|
|
b69e47 |
* was in be_bind. Since be_bind returned SLAPI_BIND_SUCCESS,
|
|
|
b69e47 |
* the entry is in the DS. So, we need to retrieve it once more.
|
|
|
b69e47 |
@@ -786,6 +798,7 @@ do_bind( Slapi_PBlock *pb )
|
|
|
b69e47 |
}
|
|
|
b69e47 |
}
|
|
|
b69e47 |
} else { /* if auto_bind || rc == slapi_bind_success | slapi_bind_anonymous */
|
|
|
b69e47 |
+ bind_failed:
|
|
|
b69e47 |
if (rc == LDAP_OPERATIONS_ERROR) {
|
|
|
b69e47 |
send_ldap_result( pb, LDAP_UNWILLING_TO_PERFORM, NULL, "Function not implemented", 0, NULL );
|
|
|
b69e47 |
goto free_and_return;
|
|
|
b69e47 |
diff --git a/ldap/servers/slapd/pw_verify.c b/ldap/servers/slapd/pw_verify.c
|
|
|
b69e47 |
index 852b027..cb182ed 100644
|
|
|
b69e47 |
--- a/ldap/servers/slapd/pw_verify.c
|
|
|
b69e47 |
+++ b/ldap/servers/slapd/pw_verify.c
|
|
|
b69e47 |
@@ -55,7 +55,7 @@ pw_verify_root_dn(const char *dn, const Slapi_Value *cred)
|
|
|
b69e47 |
int
|
|
|
b69e47 |
pw_verify_be_dn(Slapi_PBlock *pb, Slapi_Entry **referral)
|
|
|
b69e47 |
{
|
|
|
b69e47 |
- int rc = 0;
|
|
|
b69e47 |
+ int rc = SLAPI_BIND_SUCCESS;
|
|
|
b69e47 |
Slapi_Backend *be = NULL;
|
|
|
b69e47 |
|
|
|
b69e47 |
if (slapi_mapping_tree_select(pb, &be, referral, NULL, 0) != LDAP_SUCCESS) {
|
|
|
b69e47 |
@@ -109,14 +109,10 @@ pw_validate_be_dn(Slapi_PBlock *pb, Slapi_Entry **referral)
|
|
|
b69e47 |
slapi_pblock_get(pb, SLAPI_BIND_CREDENTIALS, &cred);
|
|
|
b69e47 |
slapi_pblock_get(pb, SLAPI_BIND_METHOD, &method);
|
|
|
b69e47 |
|
|
|
b69e47 |
- if (pb_sdn != NULL || cred != NULL) {
|
|
|
b69e47 |
+ if (pb_sdn == NULL) {
|
|
|
b69e47 |
return LDAP_OPERATIONS_ERROR;
|
|
|
b69e47 |
}
|
|
|
b69e47 |
|
|
|
b69e47 |
- if (*referral) {
|
|
|
b69e47 |
- return SLAPI_BIND_REFERRAL;
|
|
|
b69e47 |
- }
|
|
|
b69e47 |
-
|
|
|
b69e47 |
/* We need a slapi_sdn_isanon? */
|
|
|
b69e47 |
if (method == LDAP_AUTH_SIMPLE && cred->bv_len == 0) {
|
|
|
b69e47 |
return SLAPI_BIND_ANONYMOUS;
|
|
|
b69e47 |
@@ -130,7 +126,11 @@ pw_validate_be_dn(Slapi_PBlock *pb, Slapi_Entry **referral)
|
|
|
b69e47 |
if (slapi_mapping_tree_select(pb, &be, referral, NULL, 0) != LDAP_SUCCESS) {
|
|
|
b69e47 |
return SLAPI_BIND_NO_BACKEND;
|
|
|
b69e47 |
}
|
|
|
b69e47 |
- slapi_be_Unlock(be);
|
|
|
b69e47 |
+
|
|
|
b69e47 |
+ if (*referral) {
|
|
|
b69e47 |
+ slapi_be_Unlock(be);
|
|
|
b69e47 |
+ return SLAPI_BIND_REFERRAL;
|
|
|
b69e47 |
+ }
|
|
|
b69e47 |
|
|
|
b69e47 |
slapi_pblock_set(pb, SLAPI_BACKEND, be);
|
|
|
b69e47 |
slapi_pblock_set(pb, SLAPI_PLUGIN, be->be_database);
|
|
|
b69e47 |
@@ -138,6 +138,7 @@ pw_validate_be_dn(Slapi_PBlock *pb, Slapi_Entry **referral)
|
|
|
b69e47 |
set_db_default_result_handlers(pb);
|
|
|
b69e47 |
|
|
|
b69e47 |
/* The backend associated with this identity is real. */
|
|
|
b69e47 |
+ slapi_be_Unlock(be);
|
|
|
b69e47 |
|
|
|
b69e47 |
return SLAPI_BIND_SUCCESS;
|
|
|
b69e47 |
}
|
|
|
b69e47 |
--
|
|
|
b69e47 |
2.9.4
|
|
|
b69e47 |
|