From 70d23b8f6bbcabe2eb621ffa5009866b43e5570a Mon Sep 17 00:00:00 2001 From: Julien Rische Date: Mon, 25 Mar 2024 18:25:52 +0200 Subject: [PATCH] kdb: apply combinatorial logic for ticket flags The initial design for ticket flags was implementing this logic: * If a ticket policy is defined for the principal entry, use flags from this policy if they are set. Otherwise, use default ticket flags. * If no ticket policy is defined for the principal entry, but there is a global one, use flags from the global ticket policy if they are set. Otherwise, use default ticket flags. * If no policy (principal nor global) is defined, use default ticket flags. However, this logic was broken by a1165ffb which introduced creation of a principal-level ticket policy in case the ticket flag set is modified. This was typically the case for the -allow_tix flag, which was set virtually by the KDB driver when a user was locked until they initialize their password on first kinit pre-authentication. This was causing multiple issues, which are mitigated by the new approach: Now flags from each level are combined together. There flags like +requires_preauth which are set systematically by the KDB diver, as well as -allow_tix which is set based on the value of "nsAccountLock". This commit also adds the implicit -allow_svr ticket flag for user principals to protect users against Kerberoast-type attacks. None of these flags are stored in the LDAP database, they are hard-coded in the KDB driver. In addition to these "virtual" ticket flags, flags from both global and principal ticket policies are applied (if these policies exist). Principal ticket policies are not supported for hosts and services, but this is only an HTTP API limitation. The "krbTicketPolicyAux" object class is supported for all account types. This is required for ticket flags like +ok_to_auth_as_delegate. Such flags can be set using "ipa host-mod" and "ipa serivce-mod", or using kadmin's "modprinc". It is possible to ignore flags from the global ticket policy or default flags like -allow_svr for a user principal by setting the "final_user_tkt_flags" string attribute to "true" in kadmin. In this case, any ticket flag can be configured in the principal ticket policy, except requires_preauth and allow_tix. This fixes CVE-2024-3183 Signed-off-by: Julien Rische --- daemons/ipa-kdb/ipa_kdb.h | 43 ++++ daemons/ipa-kdb/ipa_kdb_principals.c | 306 ++++++++++++++++++++++----- util/ipa_krb5.c | 18 ++ util/ipa_krb5.h | 4 + 4 files changed, 319 insertions(+), 52 deletions(-) diff --git a/daemons/ipa-kdb/ipa_kdb.h b/daemons/ipa-kdb/ipa_kdb.h index 7baf4697f..85cabe142 100644 --- a/daemons/ipa-kdb/ipa_kdb.h +++ b/daemons/ipa-kdb/ipa_kdb.h @@ -94,6 +94,34 @@ #define IPA_KRB_AUTHZ_DATA_ATTR "ipaKrbAuthzData" #define IPA_USER_AUTH_TYPE "ipaUserAuthType" +/* Virtual managed ticket flags like "-allow_tix", are always controlled by the + * "nsAccountLock" attribute, such flags should never be set in the database. + * The following expression combine all of them, and is used to filter them + * out. */ +#define IPA_KDB_TKTFLAGS_VIRTUAL_MANAGED_ALL (KRB5_KDB_DISALLOW_ALL_TIX) + +/* Virtual static ticket flags are hard-coded in the KDB driver. */ +/* Virtual static mandatory flags are set systematically and implicitly for all + * principals. They are filtered out from database ticket flags updates. + * (However, "KRB5_KDB_REQUIRES_PRE_AUTH" can still be unset by the + * "KDC:Disable Default Preauth for SPNs" global setting) */ +#define IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_MANDATORY (KRB5_KDB_REQUIRES_PRE_AUTH) +/* Virtual static default ticket flags are implicitly set for user and non-user + * (SPN) principals, and not stored in the database. + * (Except if the "IPA_KDB_STRATTR_FINAL_TKTFLAGS" string attribute is "true" + * the principal) */ +/* Virtual static default user ticket flags are set for users only. The + * "-allow_svr" flag is set to protect them from CVE-2024-3183. */ +#define IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_DEFAULTS_USER (KRB5_KDB_DISALLOW_SVR) +#define IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_DEFAULTS_SPN (0) + +/* If this string attribute is set to "true", then only the virtual managed and + * virtual static mandatory ticket flags are applied and filtered out from + * database read and write operations for the concerned user principal. + * Configurable principal ticket flags are applied, but not the configurable + * global ticket policy flags. */ +#define IPA_KDB_STRATTR_FINAL_USER_TKTFLAGS "final_user_tkt_flags" + struct ipadb_mspac; struct dom_sid; @@ -178,6 +206,21 @@ struct ipadb_e_data { struct dom_sid *sid; }; +inline static krb5_error_code +ipadb_get_edata(krb5_db_entry *entry, struct ipadb_e_data **ied) +{ + struct ipadb_e_data *in_ied; + + in_ied = (struct ipadb_e_data *)entry->e_data; + if (!in_ied || in_ied->magic != IPA_E_DATA_MAGIC) + return EINVAL; + + if (ied) + *ied = in_ied; + + return 0; +} + struct ipadb_context *ipadb_get_context(krb5_context kcontext); int ipadb_get_connection(struct ipadb_context *ipactx); diff --git a/daemons/ipa-kdb/ipa_kdb_principals.c b/daemons/ipa-kdb/ipa_kdb_principals.c index 07cc87746..cb18861bb 100644 --- a/daemons/ipa-kdb/ipa_kdb_principals.c +++ b/daemons/ipa-kdb/ipa_kdb_principals.c @@ -706,9 +706,10 @@ static krb5_error_code ipadb_parse_ldap_entry(krb5_context kcontext, "krbTicketFlags", &result); if (ret == 0) { entry->attributes = result; - } else { - *polmask |= TKTFLAGS_BIT; } + /* Since principal, global policy, and virtual ticket flags are combined, + * they must always be resolved. */ + *polmask |= TKTFLAGS_BIT; ret = ipadb_ldap_attr_to_int(lcontext, lentry, "krbMaxTicketLife", &result); @@ -1251,23 +1252,150 @@ done: return ret; } -static krb5_flags maybe_require_preauth(struct ipadb_context *ipactx, - krb5_db_entry *entry) +static krb5_error_code +are_final_tktflags(struct ipadb_context *ipactx, krb5_db_entry *entry, + bool *final_tktflags) { - const struct ipadb_global_config *config; + krb5_error_code kerr; struct ipadb_e_data *ied; + char *str = NULL; + bool in_final_tktflags = false; - config = ipadb_get_global_config(ipactx); - if (config && config->disable_preauth_for_spns) { - ied = (struct ipadb_e_data *)entry->e_data; - if (ied && ied->ipa_user != true) { - /* not a user, assume SPN */ - return 0; - } + kerr = ipadb_get_edata(entry, &ied); + if (kerr) + goto end; + + if (!ied->ipa_user) { + kerr = 0; + goto end; + } + + kerr = krb5_dbe_get_string(ipactx->kcontext, entry, + IPA_KDB_STRATTR_FINAL_USER_TKTFLAGS, &str); + if (kerr) + goto end; + + in_final_tktflags = str && ipa_krb5_parse_bool(str); + +end: + if (final_tktflags) + *final_tktflags = in_final_tktflags; + + krb5_dbe_free_string(ipactx->kcontext, str); + return kerr; +} + +static krb5_error_code +add_virtual_static_tktflags(struct ipadb_context *ipactx, krb5_db_entry *entry, + krb5_flags *tktflags) +{ + krb5_error_code kerr; + krb5_flags vsflg; + bool final_tktflags; + const struct ipadb_global_config *gcfg; + struct ipadb_e_data *ied; + + vsflg = IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_MANDATORY; + + kerr = ipadb_get_edata(entry, &ied); + if (kerr) + goto end; + + kerr = are_final_tktflags(ipactx, entry, &final_tktflags); + if (kerr) + goto end; + + /* In practice, principal ticket flags cannot be final for SPNs. */ + if (!final_tktflags) + vsflg |= ied->ipa_user ? IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_DEFAULTS_USER + : IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_DEFAULTS_SPN; + + if (!ied->ipa_user) { + gcfg = ipadb_get_global_config(ipactx); + if (gcfg && gcfg->disable_preauth_for_spns) + vsflg &= ~KRB5_KDB_REQUIRES_PRE_AUTH; } - /* By default require preauth for all principals */ - return KRB5_KDB_REQUIRES_PRE_AUTH; + if (tktflags) + *tktflags |= vsflg; + +end: + return kerr; +} + +static krb5_error_code +get_virtual_static_tktflags_mask(struct ipadb_context *ipactx, + krb5_db_entry *entry, krb5_flags *mask) +{ + krb5_error_code kerr; + krb5_flags flags = IPA_KDB_TKTFLAGS_VIRTUAL_MANAGED_ALL; + + kerr = add_virtual_static_tktflags(ipactx, entry, &flags); + if (kerr) + goto end; + + if (mask) + *mask = ~flags; + + kerr = 0; + +end: + return kerr; +} + +/* Add ticket flags from the global ticket policy if it exists, otherwise + * succeed. If the global ticket policy is set, the "exists" parameter is set to + * true. */ +static krb5_error_code +add_global_ticket_policy_flags(struct ipadb_context *ipactx, + bool *gtpol_exists, krb5_flags *tktflags) +{ + krb5_error_code kerr; + char *policy_dn; + char *tktflags_attr[] = { "krbticketflags", NULL }; + LDAPMessage *res = NULL, *first; + int ec, ldap_tktflags; + bool in_gtpol_exists = false; + + ec = asprintf(&policy_dn, "cn=%s,cn=kerberos,%s", ipactx->realm, + ipactx->base); + if (-1 == ec) { + kerr = ENOMEM; + goto end; + } + + kerr = ipadb_simple_search(ipactx, policy_dn, LDAP_SCOPE_BASE, + "(objectclass=krbticketpolicyaux)", + tktflags_attr, &res); + if (kerr) { + if (KRB5_KDB_NOENTRY == kerr) + kerr = 0; + goto end; + } + + first = ldap_first_entry(ipactx->lcontext, res); + if (!first) { + kerr = 0; + goto end; + } + + in_gtpol_exists = true; + + ec = ipadb_ldap_attr_to_int(ipactx->lcontext, first, "krbticketflags", + &ldap_tktflags); + if (0 == ec && tktflags) { + *tktflags |= (krb5_flags)ldap_tktflags; + } + + kerr = 0; + +end: + if (gtpol_exists) + *gtpol_exists = in_gtpol_exists; + + ldap_msgfree(res); + free(policy_dn); + return kerr; } static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, @@ -1280,6 +1408,7 @@ static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, char *policy_dn = NULL; LDAPMessage *res = NULL; LDAPMessage *first; + bool final_tktflags, has_local_tktpolicy = true; int result; int ret; @@ -1288,12 +1417,18 @@ static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, return KRB5_KDB_DBNOTINITED; } + kerr = are_final_tktflags(ipactx, entry, &final_tktflags); + if (kerr) + goto done; + ret = ipadb_ldap_attr_to_str(ipactx->lcontext, lentry, "krbticketpolicyreference", &policy_dn); switch (ret) { case 0: break; case ENOENT: + /* If no principal ticket policy, fallback to the global one. */ + has_local_tktpolicy = false; ret = asprintf(&policy_dn, "cn=%s,cn=kerberos,%s", ipactx->realm, ipactx->base); if (ret == -1) { @@ -1337,12 +1472,13 @@ static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, } } if (polmask & TKTFLAGS_BIT) { - ret = ipadb_ldap_attr_to_int(ipactx->lcontext, first, - "krbticketflags", &result); - if (ret == 0) { - entry->attributes |= result; - } else { - entry->attributes |= maybe_require_preauth(ipactx, entry); + /* If global ticket policy is being applied, set flags only if + * user principal ticket flags are not final. */ + if (has_local_tktpolicy || !final_tktflags) { + ret = ipadb_ldap_attr_to_int(ipactx->lcontext, first, + "krbticketflags", &result); + if (ret == 0) + entry->attributes |= result; } } @@ -1366,13 +1502,27 @@ static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, if (polmask & MAXRENEWABLEAGE_BIT) { entry->max_renewable_life = 604800; } - if (polmask & TKTFLAGS_BIT) { - entry->attributes |= maybe_require_preauth(ipactx, entry); - } kerr = 0; } + if (polmask & TKTFLAGS_BIT) { + /* If the principal ticket flags were applied, then flags from the + * global ticket policy has to be applied atop of them if user principal + * ticket flags are not final. */ + if (has_local_tktpolicy && !final_tktflags) { + kerr = add_global_ticket_policy_flags(ipactx, NULL, + &entry->attributes); + if (kerr) + goto done; + } + + /* Virtual static ticket flags are set regardless of database content */ + kerr = add_virtual_static_tktflags(ipactx, entry, &entry->attributes); + if (kerr) + goto done; + } + done: ldap_msgfree(res); free(policy_dn); @@ -2275,6 +2425,85 @@ static krb5_error_code ipadb_get_ldap_mod_auth_ind(krb5_context kcontext, return ret; } +static krb5_error_code +update_tktflags(krb5_context kcontext, struct ipadb_mods *imods, + krb5_db_entry *entry, int mod_op) +{ + krb5_error_code kerr; + struct ipadb_context *ipactx; + struct ipadb_e_data *ied; + bool final_tktflags; + krb5_flags tktflags_mask; + int tktflags; + + kerr = ipadb_get_edata(entry, &ied); + if (kerr) + goto end; + + ipactx = ipadb_get_context(kcontext); + if (!ipactx) { + kerr = KRB5_KDB_DBNOTINITED; + goto end; + } + + kerr = get_virtual_static_tktflags_mask(ipactx, entry, &tktflags_mask); + if (kerr) + goto end; + + kerr = are_final_tktflags(ipactx, entry, &final_tktflags); + if (kerr) + goto end; + + /* Flags from the global ticket policy are filtered out only if the user + * principal flags are not final. */ + if (!final_tktflags) { + krb5_flags gtpol_tktflags = 0; + + kerr = add_global_ticket_policy_flags(ipactx, NULL, >pol_tktflags); + if (kerr) + goto end; + + tktflags_mask &= ~gtpol_tktflags; + } + + tktflags = (int)(entry->attributes & tktflags_mask); + + if (LDAP_MOD_REPLACE == mod_op && !ied->has_tktpolaux) { + if (0 == tktflags) { + /* No point initializing principal ticket policy if there are no + * flags left after filtering out virtual and global ticket policy + * ones. */ + kerr = 0; + goto end; + } + + /* if the object does not have the krbTicketPolicyAux class + * we need to add it or this will fail, only for modifications. + * We always add this objectclass by default when doing an add + * from scratch. */ + kerr = ipadb_get_ldap_mod_str(imods, "objectclass", + "krbTicketPolicyAux", LDAP_MOD_ADD); + if (kerr) + goto end; + } + + kerr = ipadb_get_ldap_mod_int(imods, "krbTicketFlags", tktflags, mod_op); + if (kerr) + goto end; + + /* If there are no custom ticket flags set in the principal, remove the + * "krbTicketFlags" attribute. */ + if (0 == tktflags) { + kerr = ipadb_get_ldap_mod_int(imods, "krbTicketFlags", tktflags, + LDAP_MOD_DELETE); + if (kerr) + goto end; + } + +end: + return kerr; +} + static krb5_error_code ipadb_entry_to_mods(krb5_context kcontext, struct ipadb_mods *imods, krb5_db_entry *entry, @@ -2350,36 +2579,9 @@ static krb5_error_code ipadb_entry_to_mods(krb5_context kcontext, /* KADM5_ATTRIBUTES */ if (entry->mask & KMASK_ATTRIBUTES) { - /* if the object does not have the krbTicketPolicyAux class - * we need to add it or this will fail, only for modifications. - * We always add this objectclass by default when doing an add - * from scratch. */ - if ((mod_op == LDAP_MOD_REPLACE) && entry->e_data) { - struct ipadb_e_data *ied; - - ied = (struct ipadb_e_data *)entry->e_data; - if (ied->magic != IPA_E_DATA_MAGIC) { - kerr = EINVAL; - goto done; - } - - if (!ied->has_tktpolaux) { - kerr = ipadb_get_ldap_mod_str(imods, "objectclass", - "krbTicketPolicyAux", - LDAP_MOD_ADD); - if (kerr) { - goto done; - } - } - } - - kerr = ipadb_get_ldap_mod_int(imods, - "krbTicketFlags", - (int)entry->attributes, - mod_op); - if (kerr) { + kerr = update_tktflags(kcontext, imods, entry, mod_op); + if (kerr) goto done; - } } /* KADM5_MAX_LIFE */ diff --git a/util/ipa_krb5.c b/util/ipa_krb5.c index 1ba6d25ee..2e663c506 100644 --- a/util/ipa_krb5.c +++ b/util/ipa_krb5.c @@ -38,6 +38,12 @@ const char *ipapwd_password_max_len_errmsg = \ TOSTR(IPAPWD_PASSWORD_MAX_LEN) \ " chars)!"; +/* Case-insensitive string values to by parsed as boolean true */ +static const char *const conf_yes[] = { + "y", "yes", "true", "t", "1", "on", + NULL, +}; + /* Salt types */ #define KRB5P_SALT_SIZE 16 @@ -1237,3 +1243,15 @@ done: } return ret; } + +bool ipa_krb5_parse_bool(const char *str) +{ + const char *const *p; + + for (p = conf_yes; *p; p++) { + if (!strcasecmp(*p, str)) + return true; + } + + return false; +} diff --git a/util/ipa_krb5.h b/util/ipa_krb5.h index 7d2ebae98..d0280940a 100644 --- a/util/ipa_krb5.h +++ b/util/ipa_krb5.h @@ -174,3 +174,7 @@ static inline bool krb5_ts_after(krb5_timestamp a, krb5_timestamp b) { return (uint32_t)a > (uint32_t)b; } + +/* Implement boolean string parsing function from MIT krb5: + * src/lib/krb5/krb/libdef_parse.c:_krb5_conf_boolean() */ +bool ipa_krb5_parse_bool(const char *str); -- 2.44.0