s4-auth: For LDAP simple bind, fall back to checking the ENCTYPE_AES256_CTS_HMAC_SHA1...
authorAndrew Bartlett <abartlet@samba.org>
Fri, 10 Jun 2022 00:47:01 +0000 (12:47 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Sun, 26 Jun 2022 22:10:29 +0000 (22:10 +0000)
Since we don't store a salt per-key, but only a single salt, when we do
not have the NT hash in the unicodePwd (eg ntlm auth = disabled), the check
will fail for a previous password if the account was renamed prior to a
newer password being set.

Pair-Programmed-With: Stefan Metzmacher <metze@samba.org>

Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Signed-off-by: Stefan Metzmacher <metze@samba.org>
Reviewed-by: Stefan Metzmacher <metze@samba.org>
auth/common_auth.h
source4/auth/ntlm/auth_sam.c
source4/auth/ntlm/wscript_build

index d922b66ab4dce3dc0b1820193e30e7474b530657..fb41d48be498cfd60c5a79e111fb1f3e601bbf47 100644 (file)
@@ -129,6 +129,9 @@ struct auth4_context {
        /* Private data for the callbacks on this auth context */
        void *private_data;
 
+       /* Kerberos context, set up on demand */
+       struct smb_krb5_context *smb_krb5_context;
+
        struct tevent_req *(*check_ntlm_password_send)(TALLOC_CTX *mem_ctx,
                                        struct tevent_context *ev,
                                        struct auth4_context *auth_ctx,
index 2a0245644176e9b5a4c26107ffaa627d017787e4..1077762c70ed3772e51cddaaab5232b800c75a8e 100644 (file)
@@ -3,7 +3,7 @@
    Password and authentication handling
    Copyright (C) Andrew Bartlett <abartlet@samba.org> 2001-2009
    Copyright (C) Gerald Carter                             2003
-   Copyright (C) Stefan Metzmacher                         2005
+   Copyright (C) Stefan Metzmacher                         2005-2010
    
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -37,6 +37,9 @@
 #include "libcli/auth/libcli_auth.h"
 #include "libds/common/roles.h"
 #include "lib/util/tevent_ntstatus.h"
+#include "system/kerberos.h"
+#include "auth/kerberos/kerberos.h"
+#include "kdc/db-glue.h"
 
 #undef DBGC_CLASS
 #define DBGC_CLASS DBGC_AUTH
@@ -53,6 +56,9 @@ extern const char *domain_ref_attrs[];
 static NTSTATUS authsam_password_ok(struct auth4_context *auth_context,
                                    TALLOC_CTX *mem_ctx,
                                    const struct samr_Password *nt_pwd,
+                                   struct smb_krb5_context *smb_krb5_context,
+                                   const DATA_BLOB *stored_aes_256_key,
+                                   const krb5_data *salt,
                                    const struct auth_usersupplied_info *user_info, 
                                    DATA_BLOB *user_sess_key, 
                                    DATA_BLOB *lm_sess_key)
@@ -63,6 +69,46 @@ static NTSTATUS authsam_password_ok(struct auth4_context *auth_context,
        case AUTH_PASSWORD_PLAIN: 
        {
                const struct auth_usersupplied_info *user_info_temp;    
+
+               if (nt_pwd == NULL && stored_aes_256_key != NULL && user_info->password.plaintext != NULL) {
+                       bool pw_equal;
+                       int krb5_ret;
+                       DATA_BLOB supplied_aes_256_key;
+                       krb5_keyblock key;
+                       krb5_data cleartext_data = {
+                               .data = user_info->password.plaintext,
+                               .length = strlen(user_info->password.plaintext)
+                       };
+
+                       *lm_sess_key = data_blob_null;
+                       *user_sess_key = data_blob_null;
+
+                       krb5_ret = smb_krb5_create_key_from_string(smb_krb5_context->krb5_context,
+                                                                  NULL,
+                                                                  salt,
+                                                                  &cleartext_data,
+                                                                  ENCTYPE_AES256_CTS_HMAC_SHA1_96,
+                                                                  &key);
+                       if (krb5_ret) {
+                               DBG_ERR("generation of a aes256-cts-hmac-sha1-96 key for password comparison failed: %s",
+                                       smb_get_krb5_error_message(smb_krb5_context->krb5_context,
+                                                                  krb5_ret, mem_ctx));
+                               return NT_STATUS_INTERNAL_ERROR;
+                       }
+
+                       supplied_aes_256_key = data_blob_const(KRB5_KEY_DATA(&key),
+                                                              KRB5_KEY_LENGTH(&key));
+
+                       pw_equal = data_blob_equal_const_time(&supplied_aes_256_key,
+                                                             stored_aes_256_key);
+
+                       krb5_free_keyblock_contents(smb_krb5_context->krb5_context, &key);
+                       if (!pw_equal) {
+                               return NT_STATUS_WRONG_PASSWORD;
+                       }
+                       return NT_STATUS_OK;
+               }
+
                status = encrypt_user_info(mem_ctx, auth_context, 
                                           AUTH_PASSWORD_HASH, 
                                           user_info, &user_info_temp);
@@ -197,6 +243,47 @@ static void auth_sam_trigger_repl_secret(TALLOC_CTX *mem_ctx,
        TALLOC_FREE(tmp_ctx);
 }
 
+static const struct samr_Password *hide_invalid_nthash(const struct samr_Password *in)
+{
+       /*
+        * This is the result of:
+        *
+        * E_md4hash("", zero_string_hash.hash);
+        */
+       static const struct samr_Password zero_string_hash = {
+               .hash = {
+                       0x31, 0xd6, 0xcf, 0xe0, 0xd1, 0x6a, 0xe9, 0x31,
+                       0xb7, 0x3c, 0x59, 0xd7, 0xe0, 0xc0, 0x89, 0xc0,
+               }
+       };
+
+       if (in == NULL) {
+               return NULL;
+       }
+
+       /*
+        * Skip over any all-zero hashes in the history.  No known software
+        * stores these but just to be sure
+        */
+       if (all_zero(in->hash, sizeof(in->hash))) {
+               return NULL;
+       }
+
+       /*
+        * This looks odd, but the password_hash module in the past has written
+        * this in the rare situation where (somehow) we didn't have an old NT
+        * hash (one of the old LM-only set paths)
+        *
+        * mem_equal_const_time() is used to avoid a timing attack
+        * when comparing secret data in the server with this constant
+        * value.
+        */
+       if (mem_equal_const_time(in->hash, zero_string_hash.hash, 16)) {
+               in = NULL;
+       }
+
+       return in;
+}
 
 /*
  * Check that a password is OK, and update badPwdCount if required.
@@ -220,7 +307,15 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con
        const char * const attrs[] = { "pwdHistoryLength", NULL };
        struct ldb_message *dom_msg;
        struct samr_Password *nt_pwd;
+       DATA_BLOB _aes_256_key = data_blob_null;
+       DATA_BLOB *aes_256_key = NULL;
+       krb5_data _salt = { .data = NULL, .length = 0 };
+       krb5_data *salt = NULL;
+       DATA_BLOB salt_data = data_blob_null;
+       struct smb_krb5_context *smb_krb5_context = NULL;
        const struct ldb_val *sc_val;
+       uint32_t userAccountControl = 0;
+       uint32_t current_kvno = 0;
        bool am_rodc;
 
        tmp_ctx = talloc_new(mem_ctx);
@@ -243,6 +338,10 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con
                return nt_status;
        }
 
+       userAccountControl = ldb_msg_find_attr_as_uint(msg,
+                                                      "userAccountControl",
+                                                      0);
+
        sc_val = ldb_msg_find_ldb_val(msg, "supplementalCredentials");
 
        if (nt_pwd == NULL && sc_val == NULL) {
@@ -273,10 +372,55 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con
                }
        }
 
-       auth_status = authsam_password_ok(auth_context, tmp_ctx,
+       /*
+        * If we don't have an NT password, pull a kerberos key
+        * instead for plaintext.
+        */
+       if (nt_pwd == NULL &&
+           sc_val != NULL &&
+           user_info->password_state == AUTH_PASSWORD_PLAIN)
+       {
+               krb5_error_code krb5_ret;
+
+               krb5_ret = smb_krb5_init_context(tmp_ctx,
+                                                auth_context->lp_ctx,
+                                                &smb_krb5_context);
+               if (krb5_ret != 0) {
+                       DBG_ERR("Failed to setup krb5_context: %s!",
+                               error_message(krb5_ret));
+                       return NT_STATUS_INTERNAL_ERROR;
+               }
+
+               /*
+                * Get the current salt from the record
+                */
+
+               krb5_ret = dsdb_extract_aes_256_key(smb_krb5_context->krb5_context,
+                                                   tmp_ctx,
+                                                   msg,
+                                                   userAccountControl,
+                                                   NULL, /* kvno */
+                                                   &current_kvno, /* kvno_out */
+                                                   &_aes_256_key,
+                                                   &salt_data);
+               if (krb5_ret == 0) {
+                       aes_256_key = &_aes_256_key;
+
+                       _salt.data = (char *)salt_data.data;
+                       _salt.length = salt_data.length;
+                       salt = &_salt;
+               }
+       }
+
+       auth_status = authsam_password_ok(auth_context,
+                                         tmp_ctx,
                                          nt_pwd,
+                                         smb_krb5_context,
+                                         aes_256_key,
+                                         salt,
                                          user_info,
                                          user_sess_key, lm_sess_key);
+
        if (NT_STATUS_IS_OK(auth_status)) {
                if (user_sess_key->data) {
                        talloc_steal(mem_ctx, user_sess_key->data);
@@ -317,7 +461,6 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con
        }
 
        for (i = 1; i < MIN(history_len, 3); i++) {
-               struct samr_Password zero_string_hash;
                const struct samr_Password *nt_history_pwd = NULL;
                NTTIME pwdLastSet;
                struct timeval tv_now;
@@ -325,54 +468,120 @@ static NTSTATUS authsam_password_check_and_record(struct auth4_context *auth_con
                int allowed_period_mins;
                NTTIME allowed_period;
 
+               /* Reset these variables back to starting as empty */
+               aes_256_key = NULL;
+               salt = NULL;
+
+               /*
+                * Obtain the i'th old password from the NT password
+                * history for this user.
+                *
+                * We avoid issues with salts (which are not
+                * recorded for historical AES256 keys) by using the
+                * ntPwdHistory in preference.
+                */
                nt_status = samdb_result_passwords_from_history(tmp_ctx,
                                                        auth_context->lp_ctx,
                                                        msg, i,
                                                        NULL,
                                                        &nt_history_pwd);
+
+               /*
+                * Belts and braces: note that
+                * samdb_result_passwords_from_history() currently
+                * does not fail for missing attributes, it only sets
+                * nt_history_pwd = NULL, so "break" and fall down to
+                * the bad password count upate if this happens
+                */
                if (!NT_STATUS_IS_OK(nt_status)) {
-                       /*
-                        * If we don't find element 'i' we won't find
-                        * 'i+1' ...
-                        */
                        break;
                }
 
+               nt_history_pwd = hide_invalid_nthash(nt_history_pwd);
+
                /*
-                * We choose to avoid any issues
-                * around different LM and NT history
-                * lengths by only checking the NT
-                * history
+                * We don't have an NT hash from the
+                * ntPwdHistory, but we can still perform the
+                * password check with the AES256
+                * key.
+                *
+                * However, this is the second preference as
+                * it will fail if the account was renamed
+                * prior to a password change (as we won't
+                * have the correct salt available to
+                * calculate the AES256 key).
                 */
-               if (nt_history_pwd == NULL) {
+
+               if (nt_history_pwd == NULL && sc_val != NULL &&
+                   user_info->password_state == AUTH_PASSWORD_PLAIN &&
+                   current_kvno >= i)
+               {
+                       krb5_error_code krb5_ret;
+                       const uint32_t request_kvno = current_kvno - i;
+
                        /*
-                        * If we don't find element 'i' we won't find
-                        * 'i+1' ...
+                        * Confirm we have a krb5_context set up
                         */
-                       break;
-               }
+                       if (smb_krb5_context == NULL) {
+                               /*
+                                * We get here if we had a unicodePwd
+                                * for the current password, no
+                                * ntPwdHistory, a valid previous
+                                * Kerberos history AND are processing
+                                * a simple bind.
+                                *
+                                * This really is a corner case so
+                                * favour cleaner code over trying to
+                                * allow for an old password.  It is
+                                * more likely this is just a new
+                                * account.
+                                *
+                                * "break" out of the loop and fall down
+                                * to the bad password update
+                                */
+                               break;
+                       }
 
-               /* Skip over all-zero hashes in the history */
-               if (all_zero(nt_history_pwd->hash,
-                            sizeof(nt_history_pwd->hash))) {
-                       continue;
-               }
+                       /*
+                        * Get the current salt from the record
+                        */
 
-               /*
-                * This looks odd, but the password_hash module writes this in if
-                * (somehow) we didn't have an old NT hash
-                */
+                       krb5_ret = dsdb_extract_aes_256_key(smb_krb5_context->krb5_context,
+                                                           tmp_ctx,
+                                                           msg,
+                                                           userAccountControl,
+                                                           &request_kvno, /* kvno */
+                                                           NULL, /* kvno_out */
+                                                           &_aes_256_key,
+                                                           &salt_data);
+                       if (krb5_ret != 0) {
+                               break;
+                       }
 
-               E_md4hash("", zero_string_hash.hash);
-               if (mem_equal_const_time(nt_history_pwd->hash, zero_string_hash.hash, 16)) {
-                       continue;
+                       aes_256_key = &_aes_256_key;
+
+                       _salt.data = (char *)salt_data.data;
+                       _salt.length = salt_data.length;
+                       salt = &_salt;
+
+               } else if (nt_history_pwd == NULL) {
+                       /*
+                        * If we don't find element 'i' in the
+                        * ntPwdHistory and can not fall back to the
+                        * kerberos hash, we won't find 'i+1' ...
+                        */
+                       break;
                }
 
                auth_status = authsam_password_ok(auth_context, tmp_ctx,
                                                  nt_history_pwd,
+                                                 smb_krb5_context,
+                                                 aes_256_key,
+                                                 salt,
                                                  user_info,
                                                  user_sess_key,
                                                  lm_sess_key);
+
                if (!NT_STATUS_IS_OK(auth_status)) {
                        /*
                         * If this was not a correct password, try the next
index f669ca90367173254689046707e8375dd8dc5116..3d96230d6af5d8ba99ed84773561b1fcc0f7d8b9 100644 (file)
@@ -4,7 +4,8 @@ bld.SAMBA_MODULE('auth4_sam_module',
        source='auth_sam.c',
        subsystem='auth4',
        init_function='auth4_sam_init',
-       deps='samdb auth4_sam NTLMSSP_COMMON samba-hostconfig RPC_NDR_IRPC MESSAGING'
+       deps='samdb auth4_sam NTLMSSP_COMMON samba-hostconfig RPC_NDR_IRPC MESSAGING db-glue',
+       enabled=bld.AD_DC_BUILD_IS_ENABLED()
        )