
Keycloakデータベース内でCVE-2026-18963(Keycloakの未認証アカウント乗っ取り)の悪用痕跡をハントする
Keycloak の PostgreSQL データベース内で CVE-2026-18963 の悪用痕跡(reset-credentials フローを介した未認証のアカウント乗っ取り)を検索する psql スクリプトです。
KYOS により公開されています。当社が運用する Keycloak デプロイメントにパッチを適用する際に、公開期間中に誰も乗っ取られていないことを検証するために作成しました。
CVE-2026-18963 は keycloak-services の reset-credentials フローにおける欠陥です(keycloak#51833)。未認証の攻撃者が、メール認証リンクをクリックすることなく任意のユーザーのパスワードリセットプロセスを完了し、その後アカウントに新しいパスワードを設定できます。ユーザーの操作は一切不要です。
使い捨てインスタンス上の Keycloak 26.7.1 に対して検証済みです。再現手順は公開しません。以下の詳細は、クエリを読むために必要な情報です。
このフローは、どの認証画面にいるかを、それを設定したステップとは結び付かない方法で追跡します。これにより、reset-credentials フローをメール認証ステップで再進入させることが可能になり、そこで脆弱な ResetCredentialEmail.action() がアクショントークンを一切確認せずに成功を返します。その後、実行はパスワード更新フォームへと続きます。
この2つの帰結が、このリポジトリのすべてのクエリを形作っています。
SEND_RESET_PASSWORD イベントを記録します。このイベントの不在はシグネチャにはなりません。action() は、戻る前にアカウントのメールアドレスを検証済みとしてマークする。 正規の経路では action() に一切到達しないため、これはバイパスによってのみ生じる状態変化です。Q3 はこれに依拠しています。修正(keycloak#51844)は、画面の状態をそれを設定した実行に結び付け、その正確なユーザーのアクショントークンが存在する場合にのみ action() を成功させます。
深刻度: Critical、CVSS v3.1 9.1(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)、Red Hat および NVD による。根本原因は認証フローにおける不適切な状態検証であり、keycloak#51844 で修正されています。
| ストリーム | 修正済み |
|---|---|
| 26.7.x | 26.7.2(リリースノート) |
| 26.6.x | 26.6.6 |
| 26.4.x(LTS) | 26.4.15 |
| 26.8 | 26.8.0 |
サポート対象ストリームにおいて、これらのバージョンより下のものはすべて脆弱です。サポート終了リリース(26.5.x、26.3 以前)には修正が提供されません。公開されたものと想定し、修正済みストリームへアップグレードしてください。Red Hat はレガシーの RH-SSO 7 を影響なしとしています。Red Hat Build of Keycloak 26.4/26.6 は 26.4.15-1 / 26.6.6-1 で修正されています。
セルフサービスのパスワードリセットを無効化すると、脆弱なエントリポイントが除去されます。すべてのレルムについて、管理コンソール > Realm settings > Login > 「Forgot password」をオフにします。管理 API 経由の場合: PUT /admin/realms/{realm} に {"resetPasswordAllowed": false} を指定します。
これにより login-actions/reset-credentials エンドポイントがブロックされますが、正規のユーザーが自身のパスワードをリセットすることもできなくなるため、アップグレードまでの暫定措置として扱ってください。Red Hat はこの CVE に対して推奨される緩和策を挙げていない点に注意してください。パッチ適用が唯一の真の修正です。
このスクリプトの最初のバージョンは、完了したリセットの直前に SEND_RESET_PASSWORD が存在しないものをフラグしていました。
それは誤りであり、
vilmosnagy がそれを指摘した。
自身のインスタンスで CVE を再現し、それでもイベントが表示されるのを確認した後だった。理由は上記の最初の帰結にある。メール認証器は正常に動作するため、被害者には本物のリセットメールが届き、Keycloak は通常の SEND_RESET_PASSWORD を記録する。チェックが反転していたのだ。それは検査した侵害アカウントすべてをクリアにしてしまっていただろう。それは削除された。
タイミング方式には、より小さいが現実的な問題がある。
thomasdelorge のハントスクリプトのアプローチは、共有された code_id 上で SEND_RESET_PASSWORD と UPDATE_PASSWORD を相関させ、短い間隔をフラグ立てする。相関キーは正しく、著者も閾値がヒューリスティックであることを明言している。我々のテストも同じ結論だ。バイパスされたリセットと、同じブラウザで完了した正当なリセットは、同一のイベント種別、同一の詳細キー、そして同じ code_id を生成する。異なるのは遅延だけであり、それによって閾値が判定全体を担うことになる。急いでいない者なら誰でもその閾値を下回る。実際のユーザーはそれを超えてしまう。なぜなら code_id が同じままであるのは、フローを開始したブラウザでリンクを開いた場合だけであり、それはメールがすでに開かれた状態にある可能性が高いケースでもあるからだ。
我々は、両者を区別するイベント、イベント詳細、エラーのいずれも見つけられなかった。「メールステップがスキップされた」イベントは存在せず、探すこともできない。存在するのは状態の副作用であり、それが Q3 で使われているものだ。
cve-2026-18963-keycloak-hunt.sql は 6 つの読み取り専用クエリを実行する:
| クエリ | 見つける内容 |
|---|---|
| Q0 | レルムがイベントを保存しているかどうか、その TTL、そしてイベントの詳細が永続化されているかどうか。Q2 は code_id 詳細で相関するため、詳細が欠けていると Q2 は盲目となり、その沈黙は何の意味も持たない。 |
| Q1 | 露出ウィンドウ内(credential.created_date)で設定されたすべてのパスワード資格情報。これが乗っ取りそのものであり、イベントの有効期限を超えて存続する。 |
| Q2 | 完了したすべての資格情報更新を、同じ認証セッションのリセットメールと相関させ、分類する。以下を参照。 |
| Q3 | 決定的な副作用: 正当な原因なしに email_verified になっているユーザー。 |
| Q4 | Admin-API による資格情報リセットとユーザー書き込み。Q1 と Q3 の行を説明し、管理者駆動の経路を除外するため。 |
| Q5 | 生のリセット/資格情報イベントのタイムライン。単一のケースを手作業で読むため。 |
| 判定 | 意味 |
|---|---|
NO_MAIL_WAS_SENT | 資格情報更新が、失敗した送信と code_id を共有している: アカウントにアドレスがない、または SMTP がそれを拒否した。リンクはサーバーから一切出ていないため、誰もそれを開けなかったはずだが、それでもフローはパスワードフォームに到達した。正当には不可能であり、悪用として扱う。 |
CLEARED_TOKEN_CLICKED | リセットが、そのユーザーのいずれの SEND_RESET_PASSWORD とも異なる code_id の下で完了した。別のブラウザやセッションで消費されたアクショントークンのみがこれを生むため、メールのリンクは実際にクリックされた。決定的に正当。 |
CANDIDATE | 送信と同じ code_id。これはバイパスがどのように見えるかであり、正当な同一ブラウザでのリセットがどのように見えるかでもある。それ自体では所見ではない。 |
NOT_RESET_FLOW | そのユーザーの SEND_RESET_PASSWORD がウィンドウ内にない。パスワードは別の方法(アカウントコンソール、管理 API、登録)で変更された。Q4 と照合すること。 |
secs_after_send は文脈のために出力される。判定として使ってはならない。
脆弱な ResetCredentialEmail.action() は、成功する前にアカウントのメールアドレスを検証済みとしてマークする。正当な経路は action() に一切入らない。なぜなら authenticate() がアクショントークンでショートサーキットするためであり、したがって本物のリセットは email_verified をそのままにする。我々はこれを 26.7.1 で、フローを開始したブラウザと別のブラウザの両方で完了した正当なリセットについて確認した。どちらも未検証のユーザーを未検証のままにした。バイパスされたリセットはどちらもフラグを反転させた。
そのため、ウィンドウ内でパスワードが変更され、現在 email_verified であり、VERIFY_EMAIL/UPDATE_EMAIL イベントもなく、それに対する管理者による書き込みもないアカウントには、検証済みであることについて無実の説明がありません。
ここで非常に重要な制限が一つあります。ウィンドウより前にすでに検証済みであったアカウントは Q3 には見えないのです。なぜなら、フラグがすでに true であったからです。Q3 は正確ではありますが、網羅的ではありません。最も強い行は verified_with_no_email = t です。なぜなら、アドレスをまったく持たないアカウントは、リセットリンクを受け取ることも、リンクを開いて検証されることもできないからです。
# 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
since を、あなたの脆弱なバージョンが稼働を開始する直前に設定します。Kubernetes では次のようにします:
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
まずQ0を確認します。events_in_window が0の場合、またはイベントが存在するのに details_with_code_id が0の場合は、Q2は機能せず、その沈黙は何も証明しません。Q1とQ3はテーブルの状態を読み取るため、イベントの保持期間の影響を受けません。
次に、順に確認していきます。
NO_MAIL_WAS_SENT。悪用です。対応してください。CLEARED_TOKEN_CLICKED。これらをクリアし、それ以上考えるのはやめてください。admin_ops_on_user が空で has_verify_email_event = f であるQ3の行は悪用です。ただし、管理者イベントを書き込まないインポート経路によって検証済みとして登録されたアカウントもここに該当するという注意点があります。CANDIDATE は未解決です。データベースではこれを確定できないため、アクセスログを確認してください。これはログが残っている限り確定的です。メールで送信されたリンクを開くことはHTTPリクエストであり、これはバイパスが偽造できない唯一のものです。リバースプロキシまたはイングレスログでは、正規のリセットは常に次のように表示されます
GET /realms/<realm>/login-actions/action-token?key=...
同じクライアントから、メールが送信されてからパスワードが設定されるまでの間。
grep -E "login-actions/action-token" access.log
Q2 の各 CANDIDATE について、SEND_RESET_PASSWORD と UPDATE_PASSWORD の間にそのリクエストがあるか探します。そこになければ、メールの関門は通過していません。
注意すべき点が 2 つあります。action-token への HEAD リクエストはメールスキャンアプライアンスから送信され、Keycloak では無視されるため、カウントされるのは GET のみです。また、一部のメールセキュリティ製品はリンクを自動的に追跡し、ユーザーが行っていない実際の GET を生成します。環境にそのような製品がある場合は、リクエスト単体ではなくユーザーエージェントと送信元アドレスで裏付けを取ってください。
最後に、最もコストの低い情報源は多くの場合ユーザー自身です。すべての被害者は、自分が要求していないパスワードリセットメールを受け取っています。短い期間と少数の候補者であれば、推測よりも直接尋ねる方が有効です。
Q1 の行は自動的に侵害を意味するわけではありません。各行を既知の正当な原因(セルフサービスリセット、ヘルプデスクの対応、新規登録)と照合し、説明のつかないものはアカウント乗っ取りの候補として扱ってください。