Samba applies a special lifetime restriction to password-change service tickets by marking kadmin/changepw requests with the internal change_pw flag and then capping the resulting service-ticket lifetime to CHANGEPW_LIFETIME. In the audited code, that classification depends on is_kadmin_changepw(), which performs case-sensitive component comparisons against "kadmin" and "changepw". This results in a policy-enforcement inconsistency in Kerberos service-principal handling. A service principal that differs only in casing can avoid the change_pw classification even though the password-change service path still processes the request as a password-change flow. As a result, the short lifetime policy is applied only to the exact lowercase spelling of the service principal, while case variants can inherit the ordinary service-ticket lifetime. This does not create a direct authorization bypass, but it weakens the intended containment property of short-lived password-change tickets and increases the usable lifetime of such tickets under case-variant service names. Affected File: https://gitlab.com/samba-team/samba/-/blob/samba-4.24.0/source4/kdc/db-glue.c Affected Code: static krb5_error_code is_kadmin_changepw(krb5_context context, krb5_const_principal principal, bool *is_changepw) { krb5_error_code ret = 0; bool eq = false; if (krb5_princ_size(context, principal) != 2) { *is_changepw = false; return 0; } ret = is_principal_component_equal(context, principal, 0, "kadmin", &eq); if (ret) { return ret; } if (!eq) { *is_changepw = false; return 0; } ret = is_principal_component_equal(context, principal, 1, "changepw", &eq); if (ret) { return ret; } *is_changepw = eq; return 0; } static krb5_error_code samba_kdc_message2entry(krb5_context context, struct samba_kdc_db_context *kdc_db_ctx, TALLOC_CTX *mem_ctx, krb5_const_principal principal, enum samba_kdc_ent_type ent_type, unsigned flags, krb5_kvno kvno, struct ldb_dn *realm_dn, struct ldb_message *msg, struct sdb_entry *entry) { [...] if (rid == DOMAIN_RID_KRBTGT) { char *realm = NULL; [...] if (ent_type == SAMBA_KDC_ENT_TYPE_SERVER) { bool is_changepw = false; ret = is_kadmin_changepw(context, principal, &is_changepw); if (ret) { goto out; } if (is_changepw && lpcfg_is_my_domain_or_realm(lp_ctx, realm)) { entry->flags.change_pw = 1; } } [...] } else if (is_rodc) { [...] } else if (entry->flags.server && ent_type == SAMBA_KDC_ENT_TYPE_SERVER) { [...] } else { [...] } [...] if (ent_type == SAMBA_KDC_ENT_TYPE_SERVER) { *entry->max_life = kdc_db_ctx->policy.svc_tkt_lifetime; } else if (ent_type == SAMBA_KDC_ENT_TYPE_KRBTGT || ent_type == SAMBA_KDC_ENT_TYPE_CLIENT) { *entry->max_life = kdc_db_ctx->policy.usr_tkt_lifetime; } else { *entry->max_life = MIN(kdc_db_ctx->policy.svc_tkt_lifetime, kdc_db_ctx->policy.usr_tkt_lifetime); } if (entry->flags.change_pw) /* Limit lifetime of kpasswd tickets to two minutes or less. */ *entry->max_life = MIN(*entry->max_life, CHANGEPW_LIFETIME); } [...] } Affected File: https://gitlab.com/samba-team/samba/-/blob/samba-4.24.0/source4/kdc/wdc-samba4.c Affected Code: static krb5_error_code samba_wdc_verify_pac(void *priv, astgs_request_t r, krb5_const_principal client_principal, hdb_entry *delegated_proxy, hdb_entry *client, hdb_entry *_server, hdb_entry *krbtgt, EncTicketPart *ticket, krb5_pac pac) { [...] } else if (!krbtgt_skdc_entry->is_trust) { /* * We expect to have received a TGT, so check that we haven't * been given a kpasswd ticket instead. We don't need to do this * check for an incoming trust, as they use a different secret * and can't be confused with a normal TGT. */ struct timeval now = krb5_kdc_get_time(); /* * Check if the ticket is in the last two minutes of its * life. */ KerberosTime lifetime = rk_time_sub(ticket->endtime, now.tv_sec); if (lifetime <= CHANGEPW_LIFETIME) { /* * This ticket has at most two minutes left to live. It * may be a kpasswd ticket rather than a TGT, so don't * accept it. */ kdc_audit_addreason((kdc_request_t)r, "Ticket is not a ticket-granting ticket"); return KRB5KRB_AP_ERR_TKT_EXPIRED; } } [...] } static krb5_error_code samba_wdc_check_client_access(void *priv, astgs_request_t r) { [...] workstation = get_netbios_name(tmp_ctx, kdc_request_get_req(r)->req_body.addresses); password_change = _server->flags.change_pw; nt_status = samba_kdc_check_client_access(client, kdc_request_get_cname((kdc_request_t)r), workstation, password_change); [...] } It is recommended to use case-insensitive principal-component comparison in is_kadmin_changepw() for both service components so that semantic variants of the password-change service principal are classified consistently. It is also recommended to make the lifetime cap depend on normalized service identity rather than raw input casing.
The MIT implementation has its own function, ks_is_kadmin_changepw().
Created attachment 19089 [details] patch for master
(not tested with MIT Kerberos)
It looks to me like we can treat this one as a normal bug.
I score the disclosed issue as AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N (4.3), being that a user can bypass the policy. I think the real impact is that an attacker who already owns the user's account, has thier TGT but not their password could get a changepw ticket that should be issued only as an initial ticket. But the user is already 'owned' in that case, and frankly getting their password if you have enough access to get the ticket can't be too hard. You could sore that as AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N (4.4) The only remaining vector might be unconstrained delegation, but that is pretty dangerous already to untrusted servers. Given comment 4 I'm moving this to the unembargoed bugs on their way out pool, in the hope we can just patch and backport, but feel free to move back if anyone is passionate for it.
(In reply to Andrew Bartlett from comment #5) The second case may not apply due to other restrictions on the krbtgt account. I also don't see this as a way back to bug 15047 (CVE-2022-2031) because the really bad part of that was presenting a kpasswd ticket as a krbtgt, and if we don't mark the ticket as change_pw it is just a normal ticket with the normal rules. The kpasswd service then checks for that with kpasswd_check_non_tgt(). Therefore I'm not even sure this is anything more than a bug, I agree with comment 4.
Removing embargo so the reviewed patch can be progressed into a public MR.
(In reply to Jennifer Sutton from comment #1) For bug 14630 I'm going to recommend we just remove this function on the MIT side, and have a single check in db_glue.c https://gitlab.com/samba-team/samba/-/merge_requests/4677
(In reply to Douglas Bagnall from comment #4) To elaborate, a ticket to kADMIN/kPASSWD is just a TGT with a funny name. We accept many other funny names for TGTs due to the need to accept. What makes a kpasswd ticket a kpasswd ticket is having a 2min lifetime (enforced at the service), and only those tickets that we acknowledge as kpasswd get permission to issue a ticket after expiry.
(In reply to Andrew Bartlett from comment #9) due to the need to accept... krbtgt/NETBIOSDOM and variations on case in the realm.