Blame SOURCES/0058-Ticket-49336-SECURITY-Locked-account-provides-differ.patch

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