GSSAPI Authentication State Confusion in OpenSSH sshd
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:
Gssctxtholds the current GSS context. In privilege-separatedsshd, the privileged monitor keeps this ingsscontext.gssapi_clientis a file-staticssh_gssapi_clientingss-serv.c. It holds the display name, exported name, mechanism pointer, and any delegated client credential. Local authorization (ssh_gssapi_userok()) and post-login credential storage read from this structure.
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.
- Mallory causes
sshdto 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_clientnow holds Alice's exported name. - On the same connection and for the same SSH user, Mallory starts a second
gssapi-with-micattempt using her own principal and an initiator that requests integrity without mutual authentication. Her context becomes the currentGssctxt;gssapi_clientis not updated. - 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:
670510daddsssh_gssapi_cleanup_global_client()and calls it from bothinput_userauth_request()andmm_answer_gss_setup_ctx()inmonitor.c. When Mallory starts her second GSS context, the monitor clears itsgssapi_clientbefore setting up the new context, so the lateruserokcall sees an empty client structure and fails with "No suitable client data".6f9cd19adds auserokflag tossh_gssapi_client, sets it on successful authorization, releases credentials whenuserokfails, and makesssh_gssapi_storecreds()a no-op unless the flag is set.
Things the Fix Did Not Change
Two other points from the report were left as they were:
ssh_gssapi_accept_ctx()still requiresGSS_C_MUTUAL_FLAGbefore callingssh_gssapi_getclient(). Mutual authentication is about the initiator authenticating the acceptor, not the other way around, so a completed non-mutual context could in principle populate the client name too. With the 10.6 cleanup in place this fails closed (a non-mutual attempt simply leavesgssapi_clientempty and cannot authenticate), though RFC 4462 non-mutual exchanges remain unsupported.- In
monitor.c,mm_answer_gss_accept_ctx()enables bothMONITOR_REQ_GSSCHECKMICandMONITOR_REQ_GSSUSEROKas soon as the context reachesGSS_S_COMPLETE, rather than waiting to enableGSSUSEROKuntil the MIC check succeeds. The unprivileged child asks for them in order, so this is only relevant if the child is already compromised.
Timeline
| Date | Event |
|---|---|
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.