OpenSSH sshd の GSSAPI 認証における状態の取り違え

CVE
CVE-2026-106555, CVE-2026-106553
CVSS
7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)
CWE
CWE-669
対象
sshd(GSSAPIAuthentication yes 時のみ。デフォルトは no)、OpenSSH 3.8p1 〜 10.4p1
修正
OpenSSH 10.6 / 10.6p1(コミット 670510d, 6f9cd19)

 openssh@openssh.com に報告
 初回パッチを確認し、権限分離プロセスの境界について返信
 upstream に修正コミット
 OpenSSH 10.6 リリース
 本メモを公開

10月6日に出た OpenSSH 10.6 に、sshd の GSSAPI(gssapi-with-mic)認証まわりの修正が2件入っています(CVE-2026-106555、CVE-2026-106553)。どちらも原因は同じで、sshd が現在の GSS コンテキストとクライアントの認可情報を別々の変数に違う寿命で持っていたために、失敗・中断した GSSAPI 認証の状態が同じ SSH 接続の後続の試行に残ってしまう、というものです。

該当箇所は 2003-11-17 の gssapi-with-mic 追加コミットから入っており、リリースとしては OpenSSH 3.8p1 以降に存在します。GSSAPIAuthentication はデフォルトで no です。

GSS 状態の持ち方

サーバ側で関係するオブジェクトは2つあります。

新しいサーバコンテキストを作るとき、Gssctxt は作り直されますが gssapi_client はそのままです:

OM_uint32
ssh_gssapi_server_ctx(Gssctxt **ctx, gss_OID oid)
{
    if (*ctx)
        ssh_gssapi_delete_ctx(ctx);
    ssh_gssapi_build_ctx(ctx);
    ssh_gssapi_set_oid(*ctx, oid);
    return ssh_gssapi_acquire_cred(*ctx);
}

一方、gssapi_client が更新されるのは ssh_gssapi_accept_ctx() の中で、コンテキスト完了時に GSS_C_MUTUAL_FLAG と GSS_C_INTEG_FLAG の両方が立っている場合(または flags == NULL の場合)に限られます:

if (((flags == NULL) || ((*flags & GSS_C_MUTUAL_FLAG) &&
    (*flags & GSS_C_INTEG_FLAG))) && (ctx->major == GSS_S_COMPLETE)) {
    if (ssh_gssapi_getclient(ctx, &gssapi_client))
        fatal("Couldn't convert client name");
}

RFC 4462 §3.1 では、gssapi-with-mic のユーザ認証に相互認証(mutual authentication)は不要であり通常は要求すべきでない、とされています。しかし OpenSSH の実装では、完全性保護あり・相互認証なしで完了したコンテキストはアクティブな Gssctxt にはなるものの、ssh_gssapi_getclient() を呼ばずに抜けます。さらに、MIC 検証の失敗時や ERRTOK 受信時、新しい SSH_MSG_USERAUTH_REQUEST が来たときにも gssapi_client をクリアする処理がありませんでした。

CVE-2026-106555: 前の試行の認可情報が残る

Alice(alice@EXAMPLE.TEST)にはローカルユーザ target へのログイン権限があり、Mallory(mallory@EXAMPLE.TEST)は対象ホストサービスの有効な Kerberos クレデンシャルは持っているが target への権限はない、とします。

  1. Mallory は何らかの方法で Alice の新鮮な GSS トークンを中継するなどして、sshd 上で Alice の相互認証・完全性保護付き GSS コンテキストを完了させ、その試行自体は MIC の段階で失敗させます。この時点で gssapi_client には Alice の名前が入っています。
  2. 同じ SSH 接続・同じ SSH ユーザ名のまま、Mallory は自分のプリンシパルで2回目の gssapi-with-mic を始めます。このとき相互認証を要求せず完全性保護のみを要求するイニシエータを使うと、Gssctxt は Mallory のものに置き換わりますが、gssapi_client は Alice のまま残ります。
  3. Mallory が自分のコンテキストで計算した正しい MIC を送ります。input_gssapi_mic() の処理は次の通りです:
gssctxt = authctxt->methoddata;

if (!GSS_ERROR(mm_ssh_gssapi_checkmic(gssctxt, &gssbuf, &mic)))
    authenticated = mm_ssh_gssapi_userok(authctxt->user);

モニター側の mm_answer_gss_checkmic() は現在の gsscontext(Mallory)に対して MIC を検証し、続く mm_answer_gss_userok() は ssh_gssapi_userok() を呼んで gssapi_client.exportedname(Alice)を krb5_kuserok() に渡します:

MIC を検証するコンテキスト: mallory@EXAMPLE.TEST
krb5_kuserok() に渡る名前:  alice@EXAMPLE.TEST

Alice の MIC を偽造したり鍵を奪ったりしているわけではなく、Mallory のコンテキストで通した MIC と、前の試行から残っていた Alice の認可情報が組み合わさる形になります。

実際に成立させるための条件はかなり窮屈です。サーバ側で GSSAPIAuthentication が有効になっていること、対象サービス宛ての Alice の新鮮な GSS トークンをどうにかして手に入れること(今回の報告ではその入手手段までは扱っていません)、Mallory 自身も同じサービスのチケットを持っていること、さらに通常の OpenSSH クライアントは常に GSS_C_MUTUAL_FLAG を立てるので自前のイニシエータを用意することが必要になります。2回の試行で SSH ユーザ名とサービス名も一致していなければなりません。

CVE-2026-106553: 失敗した試行の委任クレデンシャルが残る

ssh_gssapi_getclient() は、GSS コンテキストが完了した時点で(MIC 検証や userok より前に)委任されたクレデンシャル ctx->client_creds も gssapi_client に移します。その後 MIC が失敗しても解放されません。そして後続でいずれかの認証方式が成功すると、sshd-session.c は options.gss_authentication が有効でありさえすれば ssh_gssapi_storecreds() を呼び出しており、成功した方式が GSSAPI だったかどうかや、保存しようとしているクレデンシャルが受理された試行のものかを確認していませんでした。

そのためコード上は、失敗した GSSAPI 試行で残った委任クレデンシャルが、同じ接続で別の認証方式によりログインした後のセッションに書き出され得ます。こちらについてはソースコード上で経路を追ったのみで、実際にクレデンシャルキャッシュが作られるところまでの動的検証は行っていません。

再現確認

使い捨ての MIT Kerberos レルム(TEST.REALM)を作り、OpenSSH 10.4p1 のサーバ側 GSS オブジェクトをリンクした小さな C のテストプログラムで状態のズレを確認しました。OpenSSH 側への変更は、外から中身を表示するために gssapi_client の static を外した1箇所だけです。実行結果の抜粋:

[3] Alice context (mutual+integ)
  accept: major=0x0 flags=0x1b6 complete=1
  [after Alice (mutual+integ)]
    displayname:  alice@TEST.REALM
  OK: gssapi_client = alice (correct)

[4] Mallory context (integ only, NO mutual)
  accept: major=0x0 flags=0x134
  [after Mallory (integ only)]
    displayname:  alice@TEST.REALM
  ==> CONFIRMED: gssapi_client still = ALICE
  ==> Non-mutual Mallory did NOT overwrite Alice's identity

[5] MIC test: Mallory signs, server verifies with Mallory's ctx
  MIC verified against Mallory's server context: OK
  userok would check: alice@TEST.REALM

このプログラムは実際の OpenSSH の関数と MIT Kerberos のライブラリを呼んでいますが、稼働中の sshd に SSH パケットを送って最後まで通したものではありません。mm_ssh_gssapi_checkmic() から ssh_gssapi_userok() までの繋がりはソースコードを読んで確認しました。

10.6 での修正

最初に確認を求められたパッチは、クリーンアップ用の関数を追加して input_userauth_request() から呼ぶ形でした。ただ、現在の権限分離の構造では input_userauth_request() を実行するのは非特権の sshd-auth プロセスであるのに対し、ssh_gssapi_accept_ctx() と ssh_gssapi_userok() を実行するのは sshd-session 内の特権モニターです。sshd-auth 側で gssapi_client を消しても、そちらのプロセスにある使われていないコピーがゼロになるだけで、モニター側の gssapi_client には Alice が残ります。

その旨を返信したところ、9月16日に次の2つのコミットが入りました:

変わっていない点

報告の中で触れたうち、以下の2点は従来のままです:

タイムライン

日付内容
gssapi-with-mic のコミットでフラグ判定と MIC パスが入る(3.8p1 でリリース)
openssh@openssh.com へレポートとローカル検証コードを添付して報告
Damien Miller より修正案パッチ付きの返信。クリーンアップがモニターではなく sshd-auth 側で動いている点を返信
コミット 670510d と 6f9cd19 が upstream に反映
OpenSSH 10.6 / 10.6p1 リリース、CVE-2026-106555 と CVE-2026-106553 公開
本メモを公開

報告者: Keita Sode (SYZD Research)。修正および公開確認にご対応いただいた Damien Miller 氏に感謝します。