
Prueba de concepto y verificador de vulnerabilidades para CVE-2026-18963, una toma de control de cuenta no autenticada en el flujo de restablecimiento de credenciales de Keycloak. Python en un solo archivo, componente de navegador Playwright opcional.
Una prueba de concepto y verificador de vulnerabilidad para el bypass del flujo de restablecimiento de credenciales de Keycloak, con un componente de navegador opcional que representa el estado resultante en una ventana real de Chrome.
La herramienta es un único archivo, cve_2026_18963_poc.py, escrito contra la biblioteca estándar de Python. Playwright es la única dependencia de terceros y se importa de forma diferida, por lo que el lado HTTP se ejecuta en un intérprete estándar sin nada instalado.
cve_2026_18963_poc.py 1009 lines, Python 3.10 or newer
Crédito. CVE-2026-18963 fue reportado a Red Hat por James Paremain, quien aparece acreditado en el aviso del proveedor. Orevim no descubrió ni reportó esta vulnerabilidad. Este repositorio es una prueba de concepto independiente escrita a partir del aviso publicado.
CVE-2026-18963 es un bypass de autenticación en el flujo de contraseña olvidada de Keycloak. Son dos defectos que actúan en conjunto.
La nota sin alcance. Cuando un usuario abre la pantalla de selección de autenticador,
Keycloak escribe una nota AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED en la
sesión de autenticación. La nota es un booleano simple. Registra que una
pantalla de selección fue mostrada, no qué ejecución la mostró. La nota
sobrevive a la ejecución que la estableció.
El manejador sin protección. ResetCredentialEmail.action() establece emailVerified
en el usuario resuelto y llama a success() sin verificar el token de acción
que se supone que lleva el enlace enviado por correo. El método asume que solo se puede
llegar a él siguiendo ese enlace.
Encadenados, un llamador no autenticado establece la nota contra la ejecución
de nombre de usuario, avanza el flujo para que la ejecución de correo pase a ser la actual y luego
vuelve a entrar en la URL de restablecimiento. La nota obsoleta hace que la pantalla de selección se renderice
vinculada a la ejecución de correo. Enviar un cuerpo vacío al
loginAction de esa ejecución invoca directamente el manejador sin protección. El flujo avanza a la
acción requerida UPDATE_PASSWORD sin que se haya leído ningún correo.
| Identificador | CVE-2026-18963, GHSA-4gv3-mc9p-5wqc |
| Clase | CWE-640, mecanismo débil de recuperación de contraseña para contraseña olvidada |
| CVSS v3.1 | 9.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Afectado | >= 26.0.0, < 26.4.15, >= 26.5.0, < 26.6.6, >= 26.7.0, < 26.7.2 |
| Corregido en | 26.4.15, 26.6.6, 26.7.2. La línea 26.8.0 se distribuye con la corrección |
| Paquete | org.keycloak:keycloak-services (Maven) |
| Publicado | 18 de agosto de 2026 |
| Reportado por | James Paremain, acreditado en el aviso de Red Hat |
La única condición previa es que el realm ofrezca recuperación de contraseña a usuarios no autenticados. No se requiere ninguna credencial, token ni sesión previa.
Ocho solicitudes, en orden. Las etiquetas coinciden con las etiquetas de paso que imprime la herramienta y con los
nombres de archivo que escribe bajo --out.
| # | Etiqueta | Método | Qué hace |
|---|---|---|---|
| 1 | login | GET | URL de autorización. Establece la sesión de autenticación y genera el enlace de restablecimiento. |
| 2 | reset | GET | Página de restablecimiento de credenciales. Contiene el formulario de nombre de usuario. |
| 3 | tryanother | POST | tryAnotherWay=on. Escribe la nota sin alcance. |
| 4 | username | POST | El identificador. Avanza el flujo para que la ejecución de correo pase a ser la actual. Envía el correo de recuperación si el identificador se resuelve. |
| 5 | reenter | GET | La URL de restablecimiento de nuevo. La nota obsoleta hace que el selector se renderice vinculado a la ejecución de correo. |
| 6 | action | POST | Cuerpo vacío al loginAction de esa ejecución. Invoca el manejador sin protección. |
| 7 | required-action | GET | Sigue el Location hasta la página UPDATE_PASSWORD. |
| 8 | setpassword | POST | La nueva contraseña. Solo se envía con --set-password. |
El paso 6 es el exploit, y el cuerpo vacío es fundamental. Enviar
authenticationExecution=<id> en su lugar es la interacción ordinaria del selector: cambia
de ejecución y reenvía el correo de recuperación, lo que nunca alcanza el
defecto. El POST vacío no responde a ningún desafío, por lo que Keycloak llama al manejador para
la ejecución que la nota obsoleta dejó como actual.
# Decide whether the instance is affected. Changes nothing, sends no mail.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --check
# Additionally report which identifiers resolve to a real account.
# Resolving accounts receive a recovery email. See the note below.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --check --users-file names.txt --rate 0.25
# Reach the set-password form for one account and stop there.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --username [email protected]
# Complete the takeover. This locks the account holder out.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --username [email protected] \
--set-password 'N3w-passw0rd!'
--check sondea con un identificador aleatorio de la forma
<16 hex>@<8 hex>.invalid. El TLD .invalid está reservado por el RFC 2606 y
no puede resolverse, por lo que el sondeo no puede colisionar con una cuenta real ni
provocar el envío de correo. Establece el estado de la compilación sin tocar
a nadie.
Añadir
--userscambia eso. El paso 4 es el envío ordinario de contraseña olvidada. En un realm con SMTP configurado, un identificador que resuelve a un usuario con una dirección de correo recibe un correo genuino de recuperación de contraseña, tanto en compilaciones afectadas como parcheadas. El sondeo simple no lo hace, porqueResetCredentialEmail.authenticate()no envía nada cuando el usuario es nulo. Trata--check --userscomo un flujo que notifica a personas reales y confirma que está dentro de tu autorización antes de ejecutarlo.
La herramienta clasifica según la respuesta al paso 6, el POST vacío.
| Respuesta | Resultado | Significado |
|---|---|---|
| 400 | account-absent | El manejador se ejecutó sin token de acción y no resolvió ningún usuario. La compilación está afectada. |
302 con Location | form-reached | El manejador se ejecutó y la cuenta se resuelve. Afectada, y este identificador es real. |
| Cualquier otra cosa | not-vulnerable | El POST vacío no avanzó. |
| Sin selector en el paso 5 | not-vulnerable | La nota obsoleta no fue respetada. |
| 302 tras el paso 8 | password-set | La contraseña cambió y el realm emitió una sesión. |
| 429 superado el presupuesto de reintentos | rate-limited | Sin veredicto. La ejecución se detuvo. |
| Un negativo alcanzado tras cualquier limitación de tasa | inconclusive | Ver más abajo. |
El 400 es el importante. Separa la compilación está afectada de esta
cuenta existe, lo que permite a --check establecer la exposición contra una instancia
sin enumerar un solo usuario real. Resolver identificadores es entonces una
cuestión separada y opcional.