From dc61485ce3c08c9efa94ac74bc5b08af439f20d3 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Wed, 10 Jan 2018 10:00:27 -0700 Subject: [PATCH] winbind: account locked when using winbind refresh tickets Users were being locked out by machines they hadn't recently authenticated to. The source of the problem is in krb5_ticket_refresh_handler(), when users are password kinit'd after a long period of being offline using invalid passwords. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13212 Signed-off-by: David Mulder --- source3/libads/ads_ldap_protos.h | 2 + source3/libads/ldap.c | 27 ++++++++ source3/winbindd/winbindd.h | 1 + source3/winbindd/winbindd_cred_cache.c | 119 ++++++++++++++++++++++++++++----- source3/winbindd/winbindd_pam.c | 6 +- source3/winbindd/winbindd_proto.h | 3 +- 6 files changed, 138 insertions(+), 20 deletions(-) diff --git a/source3/libads/ads_ldap_protos.h b/source3/libads/ads_ldap_protos.h index b063815678a..e1ad7f30666 100644 --- a/source3/libads/ads_ldap_protos.h +++ b/source3/libads/ads_ldap_protos.h @@ -52,6 +52,8 @@ char **ads_pull_strings_range(ADS_STRUCT *ads, bool *more_strings); bool ads_pull_uint32(ADS_STRUCT *ads, LDAPMessage *msg, const char *field, uint32_t *v); +bool ads_pull_uint64(ADS_STRUCT *ads, LDAPMessage *msg, const char *field, + uint64_t *v); bool ads_pull_guid(ADS_STRUCT *ads, LDAPMessage *msg, struct GUID *guid); bool ads_pull_sid(ADS_STRUCT *ads, LDAPMessage *msg, const char *field, struct dom_sid *sid); diff --git a/source3/libads/ldap.c b/source3/libads/ldap.c index c18837cc524..edcb602b273 100644 --- a/source3/libads/ldap.c +++ b/source3/libads/ldap.c @@ -2743,6 +2743,33 @@ int ads_count_replies(ADS_STRUCT *ads, void *res) } /** + * pull a single uint64_t from a ADS result + * @param ads connection to ads server + * @param msg Results of search + * @param field Attribute to retrieve + * @param v Pointer to int to store result + * @return boolean inidicating success +*/ +bool ads_pull_uint64(ADS_STRUCT *ads, LDAPMessage *msg, const char *field, + uint64_t *v) +{ + char **values; + + values = ldap_get_values(ads->ldap.ld, msg, field); + if (!values) { + return False; + } + if (!values[0]) { + ldap_value_free(values); + return False; + } + + *v = strtoull(values[0], NULL, 10); + ldap_value_free(values); + return True; +} + +/** * pull a single objectGUID from an ADS result * @param ads connection to ADS server * @param msg results of search diff --git a/source3/winbindd/winbindd.h b/source3/winbindd/winbindd.h index 396f7f7946e..10057757b0f 100644 --- a/source3/winbindd/winbindd.h +++ b/source3/winbindd/winbindd.h @@ -347,6 +347,7 @@ struct WINBINDD_CCACHE_ENTRY { time_t renew_until; time_t refresh_time; struct tevent_timer *event; + NTTIME last_password_change; }; #include "winbindd/winbindd_proto.h" diff --git a/source3/winbindd/winbindd_cred_cache.c b/source3/winbindd/winbindd_cred_cache.c index 5e019dfce0c..b2273858b7f 100644 --- a/source3/winbindd/winbindd_cred_cache.c +++ b/source3/winbindd/winbindd_cred_cache.c @@ -26,6 +26,7 @@ #include "../libcli/auth/libcli_auth.h" #include "smb_krb5.h" #include "libads/kerberos_proto.h" +#include "winbindd_ads.h" #undef DBGC_CLASS #define DBGC_CLASS DBGC_WINBIND @@ -100,6 +101,61 @@ void ccache_remove_all_after_fork(void) return; } +static uint64_t fetch_last_password_change(struct WINBINDD_CCACHE_ENTRY *entry) +{ + TALLOC_CTX *ctx = talloc_new(entry); + ADS_STATUS status; + ADS_STRUCT *ads = NULL; + LDAPMessage *res = NULL; + char *ldap_exp; + uint64_t ret = 0; + const char *attr_list[] = { + "pwdLastSet", + NULL + }; + char *sam = NULL, *at_ptr; + + at_ptr = strchr(entry->principal_name, '@'); + if (at_ptr != NULL) { + int strlen = at_ptr - entry->principal_name; + sam = talloc_strndup(ctx, entry->principal_name, strlen); + } else { + DBG_DEBUG("Could not determine samAccountName from %s\n", + entry->principal_name); + goto fail; + } + + ldap_exp = talloc_asprintf(ctx, "(samAccountName=%s)", sam); + if (ldap_exp == NULL) { + goto fail; + } + + status = ads_idmap_cached_connection(&ads, entry->realm); + if (!ADS_ERR_OK(status)) { + DBG_DEBUG("Failed to fetch ads cached connection\n"); + goto fail; + } + + status = ads_do_search_all(ads, ads->config.bind_path, + LDAP_SCOPE_SUBTREE, ldap_exp, + attr_list, &res); + if (ADS_ERR_OK(status)) { + int count = ads_count_replies(ads, res); + if (count != 1) { + DBG_DEBUG("Number of fetched responses for search with" + " filter \"%s\" was %d\n", ldap_exp, count); + goto fail; + } + if (!ads_pull_uint64(ads, res, "pwdLastSet", &ret)) { + goto fail; + } + } + +fail: + TALLOC_FREE(ctx); + return ret; +} + /**************************************************************** Do the work of refreshing the ticket. ****************************************************************/ @@ -134,19 +190,34 @@ static void krb5_ticket_refresh_handler(struct tevent_context *event_ctx, rekinit: if (cred_ptr && cred_ptr->pass) { - set_effective_uid(entry->uid); - - ret = kerberos_kinit_password_ext(entry->principal_name, - cred_ptr->pass, - 0, /* hm, can we do time correction here ? */ - &entry->refresh_time, - &entry->renew_until, - entry->ccname, - False, /* no PAC required anymore */ - True, - WINBINDD_PAM_AUTH_KRB5_RENEW_TIME, - NULL); - gain_root_privilege(); + /* Using cached passwords to kinit can lockout a user + * account if their password has changed. Check the + * user pwdLastSet attribute and ignore the kinit if it + * has changed. + */ + if (fetch_last_password_change(entry) + <= entry->last_password_change) { + set_effective_uid(entry->uid); + + ret = kerberos_kinit_password_ext( + entry->principal_name, + cred_ptr->pass, + 0, /* can we do time correction here? */ + &entry->refresh_time, + &entry->renew_until, + entry->ccname, + False, /* no PAC required anymore */ + True, + WINBINDD_PAM_AUTH_KRB5_RENEW_TIME, + NULL); + + gain_root_privilege(); + } else { + ret = KRB5KDC_ERR_PREAUTH_FAILED; + DBG_NOTICE("Skipping re-kinit for %s" + " due to cached invalid password\n", + entry->principal_name); + } if (ret) { DEBUG(3,("krb5_ticket_refresh_handler: " @@ -331,9 +402,14 @@ static void krb5_ticket_gain_handler(struct tevent_context *event_ctx, goto retry_later; } - set_effective_uid(entry->uid); + /* Using cached passwords to kinit can lockout a user account if their + * password has changed. Check the user pwdLastSet attribute and ignore + * the kinit if it has changed. + */ + if (fetch_last_password_change(entry) <= entry->last_password_change) { + set_effective_uid(entry->uid); - ret = kerberos_kinit_password_ext(entry->principal_name, + ret = kerberos_kinit_password_ext(entry->principal_name, cred_ptr->pass, 0, /* hm, can we do time correction here ? */ &entry->refresh_time, @@ -343,7 +419,14 @@ static void krb5_ticket_gain_handler(struct tevent_context *event_ctx, True, WINBINDD_PAM_AUTH_KRB5_RENEW_TIME, NULL); - gain_root_privilege(); + + gain_root_privilege(); + } else { + ret = KRB5KDC_ERR_PREAUTH_FAILED; + DBG_NOTICE("Skipping kinit for %s" + " due to cached invalid password\n", + entry->principal_name); + } if (ret) { DEBUG(3,("krb5_ticket_gain_handler: " @@ -494,7 +577,8 @@ NTSTATUS add_ccache_to_list(const char *princ_name, time_t create_time, time_t ticket_end, time_t renew_until, - bool postponed_request) + bool postponed_request, + NTTIME last_password_change) { struct WINBINDD_CCACHE_ENTRY *entry = NULL; struct timeval t; @@ -625,6 +709,7 @@ NTSTATUS add_ccache_to_list(const char *princ_name, entry->renew_until = renew_until; entry->uid = uid; entry->ref_count = 1; + entry->last_password_change = last_password_change; if (!lp_winbind_refresh_tickets() || renew_until <= 0) { goto add_entry; diff --git a/source3/winbindd/winbindd_pam.c b/source3/winbindd/winbindd_pam.c index abd208a515f..fb836962387 100644 --- a/source3/winbindd/winbindd_pam.c +++ b/source3/winbindd/winbindd_pam.c @@ -767,7 +767,8 @@ static NTSTATUS winbindd_raw_kerberos_login(TALLOC_CTX *mem_ctx, time(NULL), ticket_lifetime, renewal_until, - false); + false, + info3_copy->base.last_password_change); if (!NT_STATUS_IS_OK(result)) { DEBUG(10,("winbindd_raw_kerberos_login: failed to add ccache to list: %s\n", @@ -1103,7 +1104,8 @@ static NTSTATUS winbindd_dual_pam_auth_cached(struct winbindd_domain *domain, time(NULL), time(NULL) + lp_winbind_cache_time(), time(NULL) + WINBINDD_PAM_AUTH_KRB5_RENEW_TIME, - true); + true, + my_info3->base.last_password_change); if (!NT_STATUS_IS_OK(result)) { DEBUG(10,("winbindd_dual_pam_auth_cached: failed " diff --git a/source3/winbindd/winbindd_proto.h b/source3/winbindd/winbindd_proto.h index cf01337aaad..b3bd1b6deee 100644 --- a/source3/winbindd/winbindd_proto.h +++ b/source3/winbindd/winbindd_proto.h @@ -232,7 +232,8 @@ NTSTATUS add_ccache_to_list(const char *princ_name, time_t create_time, time_t ticket_end, time_t renew_until, - bool postponed_request); + bool postponed_request, + NTTIME last_password_change); NTSTATUS remove_ccache(const char *username); struct WINBINDD_MEMORY_CREDS *find_memory_creds_by_name(const char *username); NTSTATUS winbindd_add_memory_creds(const char *username, -- 2.13.6