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

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