dsdb: Add authentication audit logging for LDAP password change
authorAndrew Bartlett <abartlet@samba.org>
Fri, 17 Mar 2017 02:58:17 +0000 (15:58 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Wed, 29 Mar 2017 00:37:29 +0000 (02:37 +0200)
This ensures this particular vector is not forgotten

Signed-off-by: Andrew Bartlett <abartlet@samba.org>
python/samba/tests/auth_log_pass_change.py
source4/dsdb/samdb/ldb_modules/password_hash.c

index 3a9311a99e110ee0f74a1e4be07eb589bde565d9..b896239c3dc8f40e9a7ee241871f159098faf3fa 100644 (file)
@@ -31,6 +31,7 @@ from samba.net import Net
 from samba import ntstatus
 import samba
 from subprocess import call
+from ldb import LdbError
 
 USER_NAME = "authlogtestuser"
 USER_PASS = samba.generate_random_password(32,32)
@@ -226,10 +227,9 @@ class AuthLogPassChangeTests(samba.tests.auth_log_base.AuthLogTestBase):
                     msg["Authentication"]["authDescription"]
                         == "OemChangePasswordUser2")
 
-        newPassword  = samba.generate_random_password(32,32)
         username     = os.environ["USERNAME"]
-        password     = os.environ["PASSWORD"]
         server       = os.environ["SERVER"]
+        password     = os.environ["PASSWORD"]
         server_param = "--server=%s" % server
         creds        = "-U%s%%%s" % (username,password)
         call(["bin/net", "rap", server_param,
@@ -240,3 +240,90 @@ class AuthLogPassChangeTests(samba.tests.auth_log_base.AuthLogTestBase):
         self.assertEquals(7,
                           len(messages),
                           "Did not receive the expected number of messages")
+
+    def test_ldap_change_password(self):
+        def isLastExpectedMessage( msg):
+            return (msg["type"] == "Authentication" and
+                    msg["Authentication"]["status"]
+                        == "NT_STATUS_OK" and
+                    msg["Authentication"]["serviceDescription"]
+                        == "LDAP Password Change" and
+                    msg["Authentication"]["authDescription"]
+                        == "LDAP Modify")
+
+        new_password = samba.generate_random_password(32,32)
+        self.ldb.modify_ldif(
+            "dn: cn=" + USER_NAME + ",cn=users," + self.base_dn + "\n" +
+            "changetype: modify\n" +
+            "delete: userPassword\n" +
+            "userPassword: " + USER_PASS + "\n" +
+            "add: userPassword\n" +
+            "userPassword: " + new_password + "\n"
+        )
+
+        messages = self.waitForMessages( isLastExpectedMessage)
+        print "Received %d messages" % len(messages)
+        self.assertEquals(4,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+    #
+    # Currently this does not get logged, so we expect to only see the log
+    # entries for the underlying ldap bind.
+    #
+    def test_ldap_change_password_bad_user(self):
+        def isLastExpectedMessage( msg):
+            return (msg["type"] == "Authorization" and
+                    msg["Authorization"]["serviceDescription"]
+                        == "LDAP" and
+                    msg["Authorization"]["authType"] == "krb5")
+
+        new_password = samba.generate_random_password(32,32)
+        try:
+            self.ldb.modify_ldif(
+                "dn: cn=" + "badUser" + ",cn=users," + self.base_dn + "\n" +
+                "changetype: modify\n" +
+                "delete: userPassword\n" +
+                "userPassword: " + USER_PASS + "\n" +
+                "add: userPassword\n" +
+                "userPassword: " + new_password + "\n"
+            )
+            self.fail()
+        except LdbError, (num, msg):
+            pass
+
+        messages = self.waitForMessages( isLastExpectedMessage)
+        print "Received %d messages" % len(messages)
+        self.assertEquals(3,
+                          len(messages),
+                          "Did not receive the expected number of messages")
+
+    def test_ldap_change_password_bad_original_password(self):
+        def isLastExpectedMessage( msg):
+            return (msg["type"] == "Authentication" and
+                    msg["Authentication"]["status"]
+                        == "NT_STATUS_WRONG_PASSWORD" and
+                    msg["Authentication"]["serviceDescription"]
+                        == "LDAP Password Change" and
+                    msg["Authentication"]["authDescription"]
+                        == "LDAP Modify")
+
+        new_password = samba.generate_random_password(32,32)
+        try:
+            self.ldb.modify_ldif(
+                "dn: cn=" + USER_NAME + ",cn=users," + self.base_dn + "\n" +
+                "changetype: modify\n" +
+                "delete: userPassword\n" +
+                "userPassword: " + "badPassword" + "\n" +
+                "add: userPassword\n" +
+                "userPassword: " + new_password + "\n"
+            )
+            self.fail()
+        except LdbError, (num, msg):
+            pass
+
+        messages = self.waitForMessages( isLastExpectedMessage)
+        print "Received %d messages" % len(messages)
+        self.assertEquals(4,
+                          len(messages),
+                          "Did not receive the expected number of messages")
index b38f1674594e66b24af5ccaf082a653755c9944f..8acebf8f0aa809ec0cad6bb3739f7e539e7b6600 100644 (file)
@@ -45,6 +45,8 @@
 #include "../lib/crypto/crypto.h"
 #include "param/param.h"
 #include "lib/krb5_wrap/krb5_samba.h"
+#include "auth/common_auth.h"
+#include "lib/messaging/messaging.h"
 
 #ifdef ENABLE_GPGME
 #undef class
@@ -119,7 +121,7 @@ struct setup_password_fields_io {
 
        struct smb_krb5_context *smb_krb5_context;
 
-       /* infos about the user account */
+       /* info about the user account */
        struct {
                uint32_t userAccountControl;
                NTTIME pwdLastSet;
@@ -128,6 +130,7 @@ struct setup_password_fields_io {
                bool is_computer;
                bool is_krbtgt;
                uint32_t restrictions;
+               struct dom_sid *account_sid;
        } u;
 
        /* new credentials and old given credentials */
@@ -2292,7 +2295,7 @@ static int setup_smartcard_reset(struct setup_password_fields_io *io)
        return LDB_SUCCESS;
 }
 
-static int make_error_and_update_badPwdCount(struct setup_password_fields_io *io)
+static int make_error_and_update_badPwdCount(struct setup_password_fields_io *io, WERROR *werror)
 {
        struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
        struct ldb_message *mod_msg = NULL;
@@ -2390,15 +2393,16 @@ static int make_error_and_update_badPwdCount(struct setup_password_fields_io *io
 
 done:
        ret = LDB_ERR_CONSTRAINT_VIOLATION;
+       *werror = WERR_INVALID_PASSWORD;
        ldb_asprintf_errstring(ldb,
                               "%08X: %s - check_password_restrictions: "
                               "The old password specified doesn't match!",
-                              W_ERROR_V(WERR_INVALID_PASSWORD),
+                              W_ERROR_V(*werror),
                               ldb_strerror(ret));
        return ret;
 }
 
-static int check_password_restrictions(struct setup_password_fields_io *io)
+static int check_password_restrictions(struct setup_password_fields_io *io, WERROR *werror)
 {
        struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
        int ret;
@@ -2406,6 +2410,8 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
                lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"),
                                         struct loadparm_context);
 
+       *werror = WERR_INVALID_PARAMETER;
+
        if (!io->ac->update_password) {
                return LDB_SUCCESS;
        }
@@ -2427,7 +2433,7 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
                   has no problems at all */
                if (io->og.nt_hash) {
                        if (!io->o.nt_hash || memcmp(io->og.nt_hash->hash, io->o.nt_hash->hash, 16) != 0) {
-                               return make_error_and_update_badPwdCount(io);
+                               return make_error_and_update_badPwdCount(io, werror);
                        }
 
                        nt_hash_checked = true;
@@ -2440,7 +2446,7 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
                if (io->og.lm_hash) {
                        if ((!io->o.lm_hash && !nt_hash_checked)
                            || (io->o.lm_hash && memcmp(io->og.lm_hash->hash, io->o.lm_hash->hash, 16) != 0)) {
-                               return make_error_and_update_badPwdCount(io);
+                               return make_error_and_update_badPwdCount(io, werror);
                        }
                }
        }
@@ -2455,10 +2461,11 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
            !io->ac->pwd_reset)
        {
                ret = LDB_ERR_CONSTRAINT_VIOLATION;
+               *werror = WERR_PASSWORD_RESTRICTION;
                ldb_asprintf_errstring(ldb,
                        "%08X: %s - check_password_restrictions: "
                        "password is too young to change!",
-                       W_ERROR_V(WERR_PASSWORD_RESTRICTION),
+                       W_ERROR_V(*werror),
                        ldb_strerror(ret));
                return ret;
        }
@@ -2481,10 +2488,11 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
 
                case SAMR_VALIDATION_STATUS_PWD_TOO_SHORT:
                        ret = LDB_ERR_CONSTRAINT_VIOLATION;
+                       *werror = WERR_PASSWORD_RESTRICTION;
                        ldb_asprintf_errstring(ldb,
                                "%08X: %s - check_password_restrictions: "
                                "the password is too short. It should be equal or longer than %u characters!",
-                               W_ERROR_V(WERR_PASSWORD_RESTRICTION),
+                               W_ERROR_V(*werror),
                                ldb_strerror(ret),
                                io->ac->status->domain_data.minPwdLength);
                        io->ac->status->reject_reason = SAM_PWD_CHANGE_PASSWORD_TOO_SHORT;
@@ -2492,26 +2500,29 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
 
                case SAMR_VALIDATION_STATUS_NOT_COMPLEX_ENOUGH:
                        ret = LDB_ERR_CONSTRAINT_VIOLATION;
+                       *werror = WERR_PASSWORD_RESTRICTION;
                        ldb_asprintf_errstring(ldb,
                                "%08X: %s - check_password_restrictions: "
                                "the password does not meet the complexity criteria!",
-                               W_ERROR_V(WERR_PASSWORD_RESTRICTION),
+                               W_ERROR_V(*werror),
                                ldb_strerror(ret));
                        io->ac->status->reject_reason = SAM_PWD_CHANGE_NOT_COMPLEX;
                        return ret;
 
                default:
                        ret = LDB_ERR_CONSTRAINT_VIOLATION;
+                       *werror = WERR_PASSWORD_RESTRICTION;
                        ldb_asprintf_errstring(ldb,
                                "%08X: %s - check_password_restrictions: "
                                "the password doesn't fit due to a miscellaneous restriction!",
-                               W_ERROR_V(WERR_PASSWORD_RESTRICTION),
+                               W_ERROR_V(*werror),
                                ldb_strerror(ret));
                        return ret;
                }
        }
 
        if (io->ac->pwd_reset) {
+               *werror = WERR_OK;
                return LDB_SUCCESS;
        }
 
@@ -2523,10 +2534,11 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
                        ret = memcmp(io->n.nt_hash, io->o.nt_history[i].hash, 16);
                        if (ret == 0) {
                                ret = LDB_ERR_CONSTRAINT_VIOLATION;
+                               *werror = WERR_PASSWORD_RESTRICTION;
                                ldb_asprintf_errstring(ldb,
                                        "%08X: %s - check_password_restrictions: "
                                        "the password was already used (in history)!",
-                                       W_ERROR_V(WERR_PASSWORD_RESTRICTION),
+                                       W_ERROR_V(*werror),
                                        ldb_strerror(ret));
                                io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY;
                                return ret;
@@ -2542,10 +2554,11 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
                        ret = memcmp(io->n.lm_hash, io->o.lm_history[i].hash, 16);
                        if (ret == 0) {
                                ret = LDB_ERR_CONSTRAINT_VIOLATION;
+                               *werror = WERR_PASSWORD_RESTRICTION;
                                ldb_asprintf_errstring(ldb,
                                        "%08X: %s - check_password_restrictions: "
                                        "the password was already used (in history)!",
-                                       W_ERROR_V(WERR_PASSWORD_RESTRICTION),
+                                       W_ERROR_V(*werror),
                                        ldb_strerror(ret));
                                io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY;
                                return ret;
@@ -2556,10 +2569,11 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
        /* are all password changes disallowed? */
        if (io->ac->status->domain_data.pwdProperties & DOMAIN_REFUSE_PASSWORD_CHANGE) {
                ret = LDB_ERR_CONSTRAINT_VIOLATION;
+               *werror = WERR_PASSWORD_RESTRICTION;
                ldb_asprintf_errstring(ldb,
                        "%08X: %s - check_password_restrictions: "
                        "password changes disabled!",
-                       W_ERROR_V(WERR_PASSWORD_RESTRICTION),
+                       W_ERROR_V(*werror),
                        ldb_strerror(ret));
                return ret;
        }
@@ -2567,10 +2581,11 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
        /* can this user change the password? */
        if (io->u.userAccountControl & UF_PASSWD_CANT_CHANGE) {
                ret = LDB_ERR_CONSTRAINT_VIOLATION;
+               *werror = WERR_PASSWORD_RESTRICTION;
                ldb_asprintf_errstring(ldb,
                        "%08X: %s - check_password_restrictions: "
                        "password can't be changed on this account!",
-                       W_ERROR_V(WERR_PASSWORD_RESTRICTION),
+                       W_ERROR_V(*werror),
                        ldb_strerror(ret));
                return ret;
        }
@@ -2578,6 +2593,87 @@ static int check_password_restrictions(struct setup_password_fields_io *io)
        return LDB_SUCCESS;
 }
 
+static int check_password_restrictions_and_log(struct setup_password_fields_io *io)
+{
+       WERROR werror;
+       int ret = check_password_restrictions(io, &werror);
+       struct ph_context *ac = io->ac;
+       /*
+        * Password resets are not authentication events, and if the
+        * upper layer checked the password and supplied the hash
+        * values as proof, then this is also not an authentication
+        * even at this layer (already logged).  This is to log LDAP
+        * password changes.
+        */
+
+       /* Do not record a failure in the auth log below in the success case */
+       if (ret == LDB_SUCCESS) {
+               werror = WERR_OK;
+       }
+
+       if (ac->pwd_reset == false && ac->change == NULL) {
+               struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
+               struct imessaging_context *msg_ctx;
+               struct loadparm_context *lp_ctx
+                       = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"),
+                                               struct loadparm_context);
+               NTSTATUS status = werror_to_ntstatus(werror);
+               const char *domain_name = lpcfg_sam_name(lp_ctx);
+               void *opaque_remote_address = NULL;
+               /*
+                * Forcing this via the NTLM auth structure is not ideal, but
+                * it is the most practical option right now, and ensures the
+                * logs are consistent, even if some elements are always NULL.
+                */
+               struct auth_usersupplied_info ui = {
+                       .mapped_state = true,
+                       .was_mapped = true,
+                       .client = {
+                               .account_name = io->u.sAMAccountName,
+                               .domain_name = domain_name,
+                       },
+                       .mapped = {
+                               .account_name = io->u.sAMAccountName,
+                               .domain_name = domain_name,
+                       },
+                       .service_description = "LDAP Password Change",
+                       .auth_description = "LDAP Modify",
+                       .password_type = "plaintext"
+               };
+
+               opaque_remote_address = ldb_get_opaque(ldb,
+                                                      "remoteAddress");
+               if (opaque_remote_address == NULL) {
+                       ldb_asprintf_errstring(ldb,
+                                              "Failed to obtain remote address for "
+                                              "the LDAP client while changing the "
+                                              "password");
+                       return LDB_ERR_OPERATIONS_ERROR;
+               }
+               ui.remote_host = talloc_get_type(opaque_remote_address,
+                                                struct tsocket_address);
+
+               msg_ctx = imessaging_client_init(ac, lp_ctx,
+                                                ldb_get_event_context(ldb));
+               if (!msg_ctx) {
+                       ldb_asprintf_errstring(ldb,
+                                              "Failed to generate client messaging context in %s",
+                                              lpcfg_imessaging_path(ac, lp_ctx));
+                       return LDB_ERR_OPERATIONS_ERROR;
+               }
+               log_authentication_event(msg_ctx,
+                                        lp_ctx,
+                                        &ui,
+                                        status,
+                                        domain_name,
+                                        io->u.sAMAccountName,
+                                        NULL,
+                                        io->u.account_sid);
+
+       }
+       return ret;
+}
+
 static int update_final_msg(struct setup_password_fields_io *io)
 {
        struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
@@ -2842,12 +2938,12 @@ static int setup_io(struct ph_context *ac,
        io->u.is_computer               = ldb_msg_check_string_attribute(info_msg, "objectClass", "computer");
 
        /* Ensure it has an objectSID too */
-       account_sid = samdb_result_dom_sid(ac, info_msg, "objectSid");
-       if (account_sid != NULL) {
+       io->u.account_sid = samdb_result_dom_sid(ac, info_msg, "objectSid");
+       if (io->u.account_sid != NULL) {
                NTSTATUS status;
                uint32_t rid = 0;
 
-               status = dom_sid_split_rid(account_sid, account_sid, NULL, &rid);
+               status = dom_sid_split_rid(account_sid, io->u.account_sid, NULL, &rid);
                if (NT_STATUS_IS_OK(status)) {
                        if (rid == DOMAIN_RID_KRBTGT) {
                                io->u.is_krbtgt = true;
@@ -3842,7 +3938,7 @@ static int password_hash_add_do_add(struct ph_context *ac)
                return ret;
        }
 
-       ret = check_password_restrictions(&io);
+       ret = check_password_restrictions_and_log(&io);
        if (ret != LDB_SUCCESS) {
                return ret;
        }
@@ -4180,7 +4276,7 @@ static int password_hash_mod_do_mod(struct ph_context *ac)
                return ret;
        }
 
-       ret = check_password_restrictions(&io);
+       ret = check_password_restrictions_and_log(&io);
        if (ret != LDB_SUCCESS) {
                return ret;
        }