Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
keycloak-cve-2026-18963-hunt — Hunt for CVE-2026-18963 exploitation traces (Keycloak unauthenticated account takeover) in the Keycloak database | Kitploit
Tools/GitHubGitHub/kyos-public/keycloak-cve-2026-18963-hunt
Vulnerability AnalysisDigital ForensicsAuthenticationIncident ResponseDatabase SecurityLog Analysis
GitHubkyos-public/keycloak-cve-2026-18963-hunt

keycloak-cve-2026-18963-hunt

Hunt for CVE-2026-18963 exploitation traces (Keycloak unauthenticated account takeover) in the Keycloak database

View Repository
1511320 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-18963: Keycloak account takeover, exploitation trace hunt

A psql script that searches a Keycloak PostgreSQL database for traces of CVE-2026-18963 exploitation (unauthenticated account takeover through the reset-credentials flow).

Published by KYOS. We wrote it while patching the Keycloak deployments we operate, to verify that nobody had been taken over during the exposure window.

The vulnerability

CVE-2026-18963 is a flaw in the reset-credentials flow of keycloak-services (keycloak#51833). An unauthenticated attacker can complete the password reset process for any user without clicking the mail verification link, then set a new password on the account. No user interaction is required.

What it means for the traces

Checked against Keycloak 26.7.1 on a disposable instance. We are not publishing reproduction steps; the details below are the ones you need to read the queries.

The flow tracks which authenticator screen it is on in a way that is not tied to the step that set it. That lets the reset-credentials flow be re-entered at the mail-verification step, where the vulnerable ResetCredentialEmail.action() returns success without ever checking for an action token. Execution then carries on to the update-password form.

Two consequences shape every query in this repo:

  • The mail step runs for real before the bypass. The victim receives a genuine password-reset email, and Keycloak writes an ordinary SEND_RESET_PASSWORD event. The absence of that event is not a signature.
  • The vulnerable action() marks the account's email address as verified before it returns. The legitimate path never reaches action() at all, so this is a state change that only the bypass produces. Q3 rests on it.

The fix (keycloak#51844) ties the screen state to the execution that set it, and only lets action() succeed when an action token for that exact user is present.

Severity: Critical, CVSS v3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N), per Red Hat and NVD. Root cause is improper state validation in the authentication flow, fixed in keycloak#51844.

Affected / fixed versions

StreamFixed in
26.7.x26.7.2 (release notes)
26.6.x26.6.6
26.4.x (LTS)26.4.15
26.826.8.0

Everything below those versions in the supported streams is vulnerable. Out-of-support releases (26.5.x, 26.3 and older) get no fix: assume exposure and upgrade to a fixed stream. Red Hat lists legacy RH-SSO 7 as not affected; the Red Hat Build of Keycloak 26.4/26.6 is fixed in 26.4.15-1 / 26.6.6-1.

Workaround if you cannot patch immediately

Disabling self-service password reset removes the vulnerable entry point: Admin console > Realm settings > Login > "Forgot password" off, for every realm. Via the admin API: PUT /admin/realms/{realm} with {"resetPasswordAllowed": false}.

This blocks the login-actions/reset-credentials endpoint but also prevents legitimate users from resetting their own password, so treat it as a stopgap until you upgrade. Note that Red Hat does not list an endorsed mitigation for this CVE; patching is the only real fix.

What does not work as a signature

The first version of this script flagged a completed reset that had no SEND_RESET_PASSWORD shortly before it.

That was wrong, and vilmosnagy pointed it out after reproducing the CVE on his own instance and watching the event appear anyway. The reason is the first consequence above: the mail authenticator runs normally, so the victim gets a real reset mail and Keycloak logs an ordinary SEND_RESET_PASSWORD. The check was inverted. It would have cleared every compromised account it looked at. It is gone.

Timing schemes have a smaller but real problem. The approach in thomasdelorge's hunt script correlates SEND_RESET_PASSWORD with UPDATE_PASSWORD on a shared code_id and flags a short gap. The correlation key is right, and the author is explicit that the threshold is a heuristic. Our testing says the same: a bypassed reset and a legitimate one completed in the same browser produce identical event types, identical detail keys and the same code_id. Only the delay differs, which leaves the threshold carrying the whole verdict. Anyone in no hurry stays under it. Real users trip it, because the code_id only stays the same when the link is opened in the browser that started the flow, which is also the case where the mail is likely already sitting open.

We found no event, no event detail and no error that separates the two. There is no "mail step skipped" event to look for. What does exist is a state side effect, and that is what Q3 uses.

What the script checks

cve-2026-18963-keycloak-hunt.sql runs six read-only queries:

QueryWhat it finds
Q0Whether the realm stores events at all, their TTL, and whether event details are being persisted. Q2 correlates on the code_id detail, so if details are missing Q2 is blind and its silence means nothing.
Q1Every password credential set inside the exposure window (credential.created_date). This is the takeover itself, and it survives event expiry.
Q2Every completed credential update, correlated with the reset mail of the same authentication session, and classified. See below.
Q3The deterministic side effect: users who are email_verified with no legitimate cause.
Q4Admin-API credential resets and user writes, to explain Q1 and Q3 rows and rule out the admin-driven route.
Q5Raw reset/credential event timeline, for reading a single case by hand.

Q2 verdicts

VerdictMeaning
NO_MAIL_WAS_SENTThe credential update shares a code_id with a failed send: no address on the account, or SMTP refused it. No link ever left the server, so nobody could have opened one, yet the flow still reached the password form. Legitimately impossible; treat as exploitation.
CLEARED_TOKEN_CLICKEDThe reset completed under a different code_id than any SEND_RESET_PASSWORD for that user. Only an action token consumed in another browser or session produces that, so the mail link really was clicked. Deterministically legitimate.
CANDIDATESame code_id as the send. This is what the bypass looks like, and also what a legitimate same-browser reset looks like. Not a finding on its own.
NOT_RESET_FLOWNo SEND_RESET_PASSWORD for that user in the window; the password changed some other way (account console, admin API, enrolment). Cross-check Q4.

secs_after_send is printed for context. Do not use it as the verdict.

Q3, the one deterministic in-database trace

The vulnerable ResetCredentialEmail.action() marks the account's email address as verified before it succeeds. The legitimate path never enters action() at all, because authenticate() short-circuits on the action token, so a genuine reset leaves email_verified alone. We checked this on 26.7.1 for legitimate resets completed both in the browser that started them and in a different one. Both left an unverified user unverified. Both bypassed resets flipped the flag.

So an account whose password changed inside the window, which is now email_verified, with no VERIFY_EMAIL/UPDATE_EMAIL event and no admin write against it, has no innocent explanation for being verified.

One limitation matters a great deal here: accounts that were already verified before the window are invisible to Q3, because the flag was already true. Q3 is precise, not exhaustive. Its strongest row is verified_with_no_email = t, because an account with no address at all can neither receive a reset link nor be verified by opening one.

Usage

root@kitploit:~
# defaults: realm 'master', window since 2026-06-01
psql -U keycloak -d keycloak -f cve-2026-18963-keycloak-hunt.sql

# explicit realm and window (run once per realm; plain values, psql quotes them)
psql -U keycloak -d keycloak \
  -v realm=myrealm -v since=2026-05-01 \
  -f cve-2026-18963-keycloak-hunt.sql

Set since to just before your vulnerable version went live. On Kubernetes:

root@kitploit:~
kubectl exec -it my-postgres-pod -- \
  psql -U keycloak -d keycloak -v realm=myrealm -v since=2026-05-01 \
  -f - < cve-2026-18963-keycloak-hunt.sql

Interpreting results

Check Q0 first. If events_in_window is 0, or details_with_code_id is 0 while events exist, Q2 cannot work and its silence proves nothing. Q1 and Q3 read table state and are unaffected by event retention.

Then work down:

  1. Q2 NO_MAIL_WAS_SENT. Exploitation. Act on it.
  2. Q2 CLEARED_TOKEN_CLICKED. Clear these and stop thinking about them.
  3. Q3 rows with an empty admin_ops_on_user and has_verify_email_event = f are exploitation, subject to the caveat that an account enrolled as verified by an import path that writes no admin event will also land here.
  4. Q2 CANDIDATE that Q3 does not corroborate is unresolved. The database cannot settle these, so go to the access log.

Settling a CANDIDATE from the access log

This is deterministic wherever the logs still exist. Opening the emailed link is an HTTP request, and it is the one thing the bypass cannot fake. In your reverse proxy or ingress log, a genuine reset always shows

root@kitploit:~
GET /realms/<realm>/login-actions/action-token?key=...

from the same client, between the mail going out and the password being set.

root@kitploit:~
grep -E "login-actions/action-token" access.log

For each CANDIDATE in Q2, look for that request between the SEND_RESET_PASSWORD and the UPDATE_PASSWORD. If it is not there, the mail gate was never passed.

Two things to watch. HEAD requests on action-token come from mail-scanning appliances and are ignored by Keycloak, so only a GET counts. And some mail security products follow links automatically, which produces a real GET the user never made; if your estate has one, corroborate with the user-agent and source address rather than the request alone.

Finally, the cheapest source is often the users themselves. Every victim received a password-reset email they never requested. For a short window and a small candidate set, asking beats inference.

Rows in Q1 are not automatically compromises. Match each against a known legitimate cause (self-service reset, helpdesk action, new enrolment) and treat anything unexplained as a takeover candidate.

If you find a takeover

  1. Disable the affected account or force a password reset through a trusted channel.
  2. Revoke the account's sessions and offline tokens (Admin console > Sessions) and rotate any secrets it could reach.
  3. Widen the investigation: reverse-proxy/ingress logs around the timestamps and IPs from Q2, actions performed by the account after the credential change, and downstream applications federated through Keycloak.

Caveats

  • Read-only, but prefer running it against a replica or a backup.
  • Written for Keycloak 26.x on PostgreSQL. Table names are stable across recent versions but verify on other databases or older versions.
  • No findings is not proof of no compromise (see Q0), especially with short event retention or logging disabled. Q3 in particular cannot see victims who were already email_verified.
  • If you write your own queries: on current Keycloak 26.x, event details are stored in event_entity.details_json_long_value, and details_json is NULL for every row. In our 26.7.1 lab it was 29 rows out of 29. Read both columns or you will silently match nothing.
  • RESET_PASSWORD is not written on a successful reset. The completion events are UPDATE_PASSWORD and UPDATE_CREDENTIAL; RESET_PASSWORD_ERROR does appear on failures.

Credits

  • @vilmosnagy for reproducing the CVE and reporting that SEND_RESET_PASSWORD is logged, which is what sent us back to the source and got the inverted check removed.
  • @thomasdelorge for the code_id correlation and the access-log check, both of which this script builds on.

License

MIT, see LICENSE. Provided as-is, without warranty.

Download Tool