CVSS 3.7 CVE-2026-60000
CVSS 3.7 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L
sshd in OpenSSH before 10.4 allows remote attackers to cause a denial of service (resource consumption from excessive authentication attempts) because MaxAuthTries was mishandled for GSSAPIAuthentication.
The root cause is a violation of RFC 4462 §3.4: when a client
sends SSH_MSG_USERAUTH_GSSAPI_ERRTOK to abort a GSSAPI exchange, the
RFC mandates that the server respond with SSH_MSG_USERAUTH_FAILURE.
OpenSSH’s input_gssapi_errtok() returned silently instead — no
FAILURE, no failures++, no rate-limit accounting — allowing an
attacker to repeat the exchange indefinitely on a single connection.
Tip
Check if a target is affected
ssh-mitm ships a standalone audit tool for the pre-authentication
username validity oracle described below — no Kerberos credentials
needed, works whether or not the RFC 4462 §3.2 fix has been backported:
$ ssh-mitm audit gssapi-usercheck-verify-patch --host <target-host>
Reports VULNERABLE, PATCHED, or INCONCLUSIVE. See Auditing this vulnerability below for the full usage, the companion enumeration command, and local test environment setup.
Scope of this CVE
The research behind this report produced four related observations, all
rooted in the same missing FAILURE response after ERRTOK. OpenSSH
fixed all of them in a single 10.4 commit, but only the
rate-limit/resource-consumption angle was listed under Security and
assigned CVE-2026-60000. The pre-authentication user-validity-oracle
observation was listed separately under Bugfixes in the same release
notes — not Security, and without a CVE — and the privileged monitor
state inconsistency and the more specific GSSSETUP resource-exhaustion
amplification were not called out individually at all. This page documents all four
together, since they share one root cause and one fix, regardless of how
OpenSSH categorised each one.
From the official OpenSSH 10.4 release notes, Security section:
“sshd(8): avoid a potential pre-authentication denial of service when GSSAPIAuthentication was enabled (this feature is off by default). This was not mitigated by MaxAuthTries, but would be penalised by PerSourcePenalties. This was reported by Manfred Kaiser of the milCERT AT (Austrian Ministry of Defence).”
—OpenSSH 10.4 release notes, Security
RFC 4462 Background
RFC 4462 defines how GSSAPI
mechanisms such as Kerberos 5 are integrated into SSH-2 authentication. The
gssapi-with-mic method proceeds in three phases:
Mechanism negotiation (§3.2): the client proposes GSSAPI mechanism OIDs; the server replies with
SSH_MSG_USERAUTH_GSSAPI_RESPONSE(type 60) for a supported OID, orSSH_MSG_USERAUTH_FAILURE(type 51) only if none are supported.Token exchange (§3.3): client and server exchange opaque tokens via
SSH_MSG_USERAUTH_GSSAPI_TOKEN(type 61) until the context is established. If the client’s own GSSAPI call fails, it sendsSSH_MSG_USERAUTH_GSSAPI_ERRTOK(type 65).Authorisation (§3.1): once the exchange completes, the server checks whether the authenticated principal is authorised for the requested account.
RFC 4462 §3.4 is unambiguous about the error path:
“If the client sends
SSH_MSG_USERAUTH_GSSAPI_ERRTOK, the server MUST send anSSH_MSG_USERAUTH_FAILUREmessage.”
A FAILURE response implies a call to userauth_finish(), which
increments authctxt->failures, eventually disconnects the client once
MaxAuthTries is exhausted, feeds PerSourcePenalties accumulation
(OpenSSH ≥ 9.8), and emits an auth_log() entry. OpenSSH did none of these.
Vulnerable Code
input_gssapi_errtok() in auth2-gss.c processed the error token and
returned silently:
/* auth2-gss.c — input_gssapi_errtok(), before the 10.4 fix */
static int
input_gssapi_errtok(int type, uint32_t plen, struct ssh *ssh)
{
/* ... parse recv_tok ... */
maj_status = mm_ssh_gssapi_accept_ctx(gssctxt, &recv_tok,
&send_tok, NULL);
free(recv_tok.value);
ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_GSSAPI_TOKEN, NULL);
ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_GSSAPI_ERRTOK, NULL);
/* The client will have already moved on to the next auth */
gss_release_buffer(&maj_status, &send_tok);
return 0;
/* ↑ no userauth_finish(), no FAILURE, no failures++, postponed=1 */
}
The comment — “The client will have already moved on to the next auth” —
captures the design assumption: ERRTOK always precedes a fresh
USERAUTH_REQUEST. The RFC-mandated FAILURE was simply never sent.
Bypass Flow
sequenceDiagram
participant C as Client (attacker)
participant S as OpenSSH Server (< 10.4)
loop Unlimited — failures++ never reached
C->>S: USERAUTH_REQUEST (50)<br/>gssapi-with-mic · valid user
S-->>C: GSSAPI_RESPONSE (60)<br/>postponed=1
C->>S: USERAUTH_GSSAPI_ERRTOK (65)<br/>dummy token
Note over S: input_gssapi_errtok() returns 0<br/>no FAILURE sent · failures unchanged
end
Note right of C: MaxAuthTries: not consumed<br/>PerSourcePenalties: not triggered per cycle<br/>auth.log: no entry at LogLevel INFO
Bypassed Defences
Defence |
Normal behaviour |
With the ERRTOK bug |
|---|---|---|
|
Disconnect after 6 failures |
Not consumed — |
|
+5 s per failure; IP blocked after threshold |
Applied but insufficient — the |
|
Disconnect if not authenticated in time |
Not bypassed — the grace timer is armed once per connection and
fires independently of ERRTOK activity. At the cycle rate this bug
permits, the hard |
Hard |
N/A — normally unreachable before |
The actual bound — terminates the connection after exhausting
the attempt budget, independently of |
|
|
No entry — |
Empirical confirmation of the table above, from a 1,023-cycle single-connection
run against an unpatched server with strace attached to the privileged
monitor process:
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_status=255 ...} ---
kill(<child pid>, SIGKILL) = -1 ESRCH (No such process)
exit_group(5) = ?
+++ exited with 5 +++
Exit code 5 is EXIT_AUTH_ATTEMPTED — the code the monitor process
substitutes for a plain 255 once it detects that authentication was
attempted before the connection was torn down. This, not EXIT_LOGIN_GRACE
(code 3), is what child_reap() observes, confirming LoginGraceTime
never fired and the hard attempt ceiling terminated the connection first.
Repeating the same loop sequentially (one connection at a time, each run to exhaustion) never activates a block, confirmed in the target’s own syslog:
sshd-session[...]: error: maximum authentication attempts exceeded for ... [preauth]
sshd-session[...]: error: maximum authentication attempts exceeded for ... [preauth]
sshd-session[...]: error: maximum authentication attempts exceeded for ... [preauth]
[no "srclimit_penalise: ... activating" line appears — the source IP is never blocked]
Four parallel connections from the same source, by contrast, do accumulate past the activation threshold and trigger a block once all four close:
sshd[...]: srclimit_penalise: 127.0.0.1/32: activating ipv4 penalty of 19.898 seconds
for penalty: failed authentication
Auditing this vulnerability
ssh-mitm audit ships two commands for this finding, both requiring only
GSSAPIAuthentication yes and a Kerberos mechanism library (libkrb5)
on the target — no valid Kerberos credentials, since the oracle triggers
before any real exchange happens:
# Enumerate specific usernames
$ ssh-mitm audit gssapi-usercheck --host <target-host> \
--username alice bob admin
# Check whether the target is vulnerable, patched, or backported —
# no target-specific usernames required
$ ssh-mitm audit gssapi-usercheck-verify-patch --host <target-host>
The verification command works without knowing any account on the target: it
probes built-in near-universal candidates (root, daemon) alongside an
automatically generated, guaranteed-nonexistent control username, and
compares the responses. If the control username’s response differs from the
others, the oracle is still active; if every response is identical, the
server no longer distinguishes valid from invalid usernames at this stage.
A fixed guess such as plain notexist, or a standard system account such
as nobody, would not be a reliable control — OpenSSH’s allowed_user()
only requires a present and executable shell to treat an account as valid at
this stage, and nologin/false shells qualify, so such accounts
routinely exist and would silently invalidate the comparison.
For local test environment setup (with or without a full Kerberos deployment), see gssapi-with-mic authentication.
Test Environment Setup
To reproduce the exit-code evidence in the Bypassed Defences table above, run the ERRTOK loop to exhaustion on a single connection while tracing the privileged monitor process:
# In one terminal, once the connection has been accepted, attach to the
# per-connection monitor process (sshd-session, not the sshd listener):
sudo strace -p $(pgrep -f 'sshd-session.*priv' | tail -1) \
-e trace=exit_group,kill -f
# Confirm the syslog attribution independently:
sudo journalctl -u sshd --since "5 minutes ago" | grep -i 'maximum auth\|activating'
The Official Fix
OpenSSH 10.4 fixed all four observations in a single commit, 5d04ca6af7, credited directly to this research:
upstream: Fix multiple RFC 4462 (GSSAPIAuthentication) compliance problems
1) Remove an early failure return for GSSAPI authentication attempts made for invalid accounts that yielded different behaviour for valid vs invalid accounts.
2) Fix a situation where some GSSAPI requestes were not correctly subjected to MaxAuthTries.
Fix a moderate pre-authentication resource DoS related to #2.
Add missing logging for error cases.
Report and fixes from Manfred Kaiser, milCERT AT
The diff confirms the fix addresses all four observations at once:
the early
!authctxt->validreturn inuserauth_gssapi()is removed entirely — closing the user-validity-oracle signal (§3.2);input_gssapi_errtok()now callsuserauth_finish(ssh, 0, "gssapi-with-mic", NULL)instead of returning silently — restoringfailures++/MaxAuthTriesaccounting and, as a side effect, ending the unlimitedGSSSETUPcycling that caused the resource amplification;input_gssapi_errtok()no longer forwards the token to the monitor viamm_ssh_gssapi_accept_ctx()at all — it now only cancels the exchange and logs — which also eliminates the post-GSS_S_COMPLETEmonitor state inconsistency, sinceMONITOR_REQ_GSSSTEPis never reached from this path anymore;a
logit()call was added so failed GSSAPI attempts are now visible at the defaultLogLevel INFO, closing the stealth gap described above.
/* auth2-gss.c, input_gssapi_errtok() — as fixed in OpenSSH 10.4 */
logit("Failed gssapi-with-mic for %s%.100s from %.200s port %d ssh2",
authctxt->valid ? "" : "invalid user ",
authctxt->user,
ssh_remote_ipaddr(ssh), ssh_remote_port(ssh));
authctxt->postponed = 0;
ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_GSSAPI_TOKEN, NULL);
ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_GSSAPI_ERRTOK, NULL);
ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_GSSAPI_MIC, NULL);
ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, NULL);
userauth_finish(ssh, 0, "gssapi-with-mic", NULL);
return 0;
A same-day follow-up, c10e049803
(“upstream: unused variables”, 2026-07-07), removes the p/len
variables from input_gssapi_errtok() that became dead once the function
stopped parsing the token via sshpkt_get_string(). This landed one day
after the 10.4 tag and is a compile-cleanliness follow-up, not a functional
change.
Mitigation
Upgrade to OpenSSH 10.4 or later. The fix restores MaxAuthTries
accounting and PerSourcePenalties for all four observations at once, and
enforces the minimum authentication delay for the postponed completion paths
fixed by the Orange Cyberdefense report.
Where an immediate upgrade is not possible, disable GSSAPI authentication entirely if Kerberos single sign-on is not required:
# /etc/ssh/sshd_config
GSSAPIAuthentication no
References
openssh/openssh-portable — the fix
openssh/openssh-portable — same-day dead-variable cleanup follow-up
openssh/openssh-portable — related Orange Cyberdefense auth-delay fix
https://nvd.nist.gov/vuln/detail/CVE-2026-60001 — the Orange Cyberdefense finding’s own CVE record
https://www.rfc-editor.org/rfc/rfc4462#section-3.4 — RFC 4462 §3.4: server MUST send
FAILUREafterERRTOKhttps://www.rfc-editor.org/rfc/rfc4462#section-3.2 — RFC 4462 §3.2: server MUST send
GSSAPI_RESPONSEfor a supported OIDhttps://nvd.nist.gov/vuln/detail/CVE-2018-15473 — introduced the branch point exploited for the user validity oracle
https://nvd.nist.gov/vuln/detail/CVE-2018-15919 — related but distinct technique for the same purpose (
failurescounter)CVSS 5.3 CVE-2016-20012 — comparable pre-authentication user validity oracle via public-key probing