Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
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.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-18963 — Docker-based lab and Python exploit for CVE-2026-18963, a Keycloak reset-credentials flow bypass enabling account takeover via email verification bypass. | Kitploit
Tools/GitHubGitHub/ivanesk315/cve-2026-18963
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration TestingIdentity & Access Management (IAM)AuthenticationLearning & EducationLabs & Practice
GitHubivanesk315/cve-2026-18963

CVE-2026-18963

Docker-based lab and Python exploit for CVE-2026-18963, a Keycloak reset-credentials flow bypass enabling account takeover via email verification bypass.

281 month agoNot yet reviewed
View Repository

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 Reset-Credentials Flow Bypass Lab

Overview

Lab simulating vulnerability CVE-2026-18963 (CVSS 9.1) in Keycloak, allowing an attacker to take over any account through bypassing email verification in the password reset flow.

For security research and educational purposes only.

Requirements

  • Docker & Docker Compose
  • Python 3.8+
  • pip

Usage Instructions

1. Start vulnerable Keycloak

docker-compose up -d

Wait for Keycloak to start (~30-60 seconds).

2. Set up the lab

pip install -r requirements.txt
python setup-lab.py

Script sẽ tạo:

  • Realm vuln-lab với reset-password enabled
  • Cấu hình SMTP (MailHog) để gửi email
  • User victim ([email protected] / VictimPass123!)

3. Chạy exploit

python exploit.py -u http://127.0.0.1:8080 -r vuln-lab -t victim -p Pwned123!

Options:

  • -u / --url: Keycloak URL (default: http://127.0.0.1:8080)
  • -r / --realm: Realm name (default: vuln-lab)
  • -t / --target: Target username (default: victim)
  • -p / --password: New password (default: Pwned123!)
  • -v / --verbose: Enable debug output

4. View email (optional)

MailHog UI: http://127.0.0.1:8025 — view the reset-password email sent during the exploit.

5. Cleanup

docker-compose down -v

Technical details

Root cause

Two combined flaws in Keycloak form an attack chain:

Flaw 1 — Selector state corruption (DefaultAuthenticationFlow.java): When the user clicks "Try Another Way", the auth note AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED is stored as "true" (a boolean string) instead of the execution model ID. The value "true" is not bound to any specific execution, so it persists across steps in the flow, causing the selector to display the wrong context.

Flaw 2 — Unconditional action success (ResetCredentialEmail.java): The action() method of ResetCredentialEmail calls context.success() unconditionally without validating the action token. Normally, action() is only called when the user clicks the link in the email (which carries an action token). But when the selector is corrupted, an attacker can trigger action() directly through flow processing.

Detailed attack flow

Attacker                              Keycloak
   │                                     │
   │─── GET /auth (OIDC + PKCE) ────────>│  1. Khởi tạo auth session
   │<── Login page + cookies ────────────│
   │                                     │
   │─── GET /reset-credentials ─────────>│  2. Chuyển đến reset flow
   │<── Username form ──────────────────│
   │                                     │
   │─── POST tryAnotherWay=on ─────────>│  3. Corrupt selector state
   │<── Authenticator selector ─────────│     SELECTOR_DISPLAYED = "true"
   │                                     │
   │─── POST username=victim ──────────>│  4. Gửi username qua selector
   │<── "Check your email" page ────────│     Email gửi, CURRENT_EXEC = email_id
   │                                     │
   │─── GET /reset-credentials ────────>│  5. Re-enter reset flow
   │<── Corrupted selector (!!!) ───────│     processFlow() thấy SELECTOR="true"
   │                                     │     → hiển thị selector cho email step
   │                                     │
   │─── POST {} (empty body) ──────────>│  6. BYPASS: trigger action()
   │<── 302 → UPDATE_PASSWORD ─────────│     processAction() không thấy
   │                                     │     authenticationExecution trong form
   │─── GET /required-action ──────────>│     → rơi vào nhánh action()
   │<── Password update form ──────────│     ResetCredentialEmail.action()
   │                                     │     → context.success() (vô điều kiện!)
   │                                     │     → flow chuyển sang ResetPassword
   │                                     │
   │─── POST password-new=Pwned! ──────>│  7. Đặt mật khẩu mới
   │<── 302 → /account/ ──────────────│     Account takeover hoàn tất
   │                                     │
   └── Đăng nhập với mật khẩu mới ──────┘

Why does step 6 work?

In DefaultAuthenticationFlow.processAction(), when receiving a POST:

  1. Check tryAnotherWay in the form → NO (form is empty)
  2. Check authenticationExecution in the form → NO (form is empty)
  3. Fall into the last branch: call authenticator.action(result) on the model from the URL

Because the URL contains execution=<email_exec_id> (from the selector form action), ResetCredentialEmail.action() is called → returns context.success() → the flow moves to ResetPassword → displays the password setup form.

Affected versions

ProductAffectedPatched
Keycloak (upstream)< 26.7.226.7.2+
RHBK 26.4.x< 26.4.1526.4.15+
RHBK 26.6.x< 26.6.626.6.6+

Patch (PR #51844)

DefaultAuthenticationFlow.java:

  • setAuthNote(SELECTOR_DISPLAYED, "true") → setAuthNote(SELECTOR_DISPLAYED, model.getId())
  • processFlow() checks selector.equals(lastExecutionId) instead of Boolean.parseBoolean()
  • If it does not match → removeAuthNote(SELECTOR_DISPLAYED)

ResetCredentialEmail.java:

  • action() checks ACTION_TOKEN_USER_ID before calling context.success()
  • If there is no valid action token → context.failure(INVALID_USER)

References

  • NVD - CVE-2026-18963
  • Red Hat CVE Page
  • Keycloak Issue #51833
  • Fix PR #51844
  • Kudelski Security Research
  • The Hacker News
Download Tool