GSSAPI Authentication State Confusion in OpenSSH sshd

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
Affected
sshd with GSSAPIAuthentication yes (off by default), OpenSSH 3.8p1 through 10.4p1
Fix
OpenSSH 10.6 / 10.6p1 (commits 670510d, 6f9cd19)

: Reported to openssh@openssh.com
: Initial patch reviewed; sent note on privilege-separation process boundary
: Fixes committed upstream
: OpenSSH 10.6 released
: Published these notes

OpenSSH 10.6, released on October 6, includes two fixes for sshd's GSSAPI (gssapi-with-mic) authentication path (CVE-2026-106555 and CVE-2026-106553). Both come down to the same issue: sshd kept the active GSS cryptographic context and the parsed client identity in separate variables with different lifetimes, so state from a failed or abandoned GSSAPI attempt could carry over into a later one on the same SSH connection.

The affected code was introduced in the 2003-11-17 gssapi-with-mic commit and first shipped in OpenSSH 3.8p1. GSSAPIAuthentication defaults to no.

How sshd Stores GSS State

On the server side, two objects matter:

Starting a new server context frees and replaces Gssctxt, but does not touch 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 is populated inside ssh_gssapi_accept_ctx(), and only when the completed context has both GSS_C_MUTUAL_FLAG and GSS_C_INTEG_FLAG set (or when 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 notes that mutual authentication is not needed for gssapi-with-mic user authentication and should normally not be requested. In OpenSSH, a context that completes with integrity but without mutual authentication still becomes the active Gssctxt, yet skips ssh_gssapi_getclient() and leaves whatever was already in gssapi_client untouched. Nothing else cleared gssapi_client on MIC failure, on ERRTOK, or when a new SSH_MSG_USERAUTH_REQUEST arrived.

CVE-2026-106555: Stale Identity Across Attempts

Suppose Alice (alice@EXAMPLE.TEST) is authorized for local user target and Mallory (mallory@EXAMPLE.TEST) has a valid Kerberos credential for the host service but is not authorized for target.

  1. Mallory causes sshd to complete a mutual, integrity-protected GSS context for Alice (for example by relaying one fresh Alice token), then lets that attempt fail at the MIC step. gssapi_client now holds Alice's exported name.
  2. On the same connection and for the same SSH user, Mallory starts a second gssapi-with-mic attempt using her own principal and an initiator that requests integrity without mutual authentication. Her context becomes the current Gssctxt; gssapi_client is not updated.
  3. Mallory sends a valid MIC computed with her own context. input_gssapi_mic() runs:
gssctxt = authctxt->methoddata;

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

In the monitor, mm_answer_gss_checkmic() verifies the MIC against the current gsscontext (Mallory's), while mm_answer_gss_userok() calls ssh_gssapi_userok(), which passes gssapi_client.exportedname (Alice's) to krb5_kuserok():

MIC verified against:     mallory@EXAMPLE.TEST
krb5_kuserok() checks:    alice@EXAMPLE.TEST

This does not forge Alice's MIC or recover her key; it pairs a valid MIC from one context with the cached identity from an earlier one.

The prerequisites are restrictive. GSSAPIAuthentication must be enabled, Mallory needs a fresh Alice GSS token for the target service (my report did not cover how to obtain one) plus her own service ticket, and she has to use a custom initiator because the stock OpenSSH client always sets GSS_C_MUTUAL_FLAG. Both attempts also have to use the same SSH username and service.

CVE-2026-106553: Delegated Credentials From a Failed Attempt

ssh_gssapi_getclient() also moves any delegated credential (ctx->client_creds) into gssapi_client as soon as the GSS context completes, before MIC verification and userok run. A failed MIC did not release it. Later, once any authentication method succeeded, sshd-session.c called ssh_gssapi_storecreds() whenever options.gss_authentication was set, without checking whether GSSAPI was the method that succeeded or whether the stored credential came from an accepted attempt.

From the source, a delegated credential left behind by a failed GSSAPI attempt could therefore be written out when the connection subsequently authenticated by another method. I only traced this path in the source and did not dynamically reproduce credential-cache creation.

Reproduction

I verified the state mismatch with a small C harness linked against OpenSSH 10.4p1's server-side GSS objects in a disposable MIT Kerberos realm (TEST.REALM). The only modification to the OpenSSH source was dropping static on gssapi_client so the test could read it directly. Output from the run:

[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

The harness calls the real OpenSSH functions and MIT Kerberos library routines, but does not drive the exchange over the wire against a running sshd. The step from mm_ssh_gssapi_checkmic() to ssh_gssapi_userok() was checked by reading the source.

The Fix in 10.6

The first patch I was asked to test added a cleanup helper and called it from input_userauth_request(). In the current privilege-separated layout, input_userauth_request() runs in the unprivileged sshd-auth child, whereas ssh_gssapi_accept_ctx() and ssh_gssapi_userok() run in the privileged monitor inside sshd-session. Clearing gssapi_client in sshd-auth only zeroes that process's unused copy; the monitor's copy still holds Alice.

After I pointed that out, two commits went into the tree on September 16:

Things the Fix Did Not Change

Two other points from the report were left as they were:

Timeline

DateEvent
gssapi-with-mic commit introduces the flag check and MIC path (shipped in 3.8p1)
Reported to openssh@openssh.com with report and local harness
Reply from Damien Miller with candidate patch; replied noting the cleanup ran in sshd-auth rather than the monitor
Commits 670510d and 6f9cd19 pushed upstream
OpenSSH 10.6 / 10.6p1 released; CVE-2026-106555 and CVE-2026-106553 published
Published these notes

Reporter: Keita Sode (SYZD Research). Thanks to Damien Miller for the fix and for confirming that these details could be published.