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:

  1. 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, or SSH_MSG_USERAUTH_FAILURE (type 51) only if none are supported.

  2. 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 sends SSH_MSG_USERAUTH_GSSAPI_ERRTOK (type 65).

  3. 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 an SSH_MSG_USERAUTH_FAILURE message.”

RFC 4462 §3.4

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

MaxAuthTries (default: 6)

Disconnect after 6 failures

Not consumedfailures++ never reached

PerSourcePenalties (≥ 9.8)

+5 s per failure; IP blocked after threshold

Applied but insufficient — the AUTHFAIL penalty (5 s) does fire once per connection, at connection close, since a connection terminated after exhausting the attempt cap below is classified as an auth-attempted exit. It does not accumulate per ERRTOK cycle. A single connection exhausting the attempt cap runs for roughly a minute before closing, which exceeds the 5 s penalty’s own lifetime — so for a sequentially-repeated attacker connection, each connection’s penalty has already expired by the time the next one closes, and the accumulator never crosses the activation threshold needed to actually block the source.

LoginGraceTime

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 attempt ceiling below is reached first; on a slower connection LoginGraceTime would terminate it regardless.

Hard attempt ceiling (1024)

N/A — normally unreachable before MaxAuthTries disconnects first

The actual bound — terminates the connection after exhausting the attempt budget, independently of MaxAuthTries

auth_log() at LogLevel INFO

Failed / Invalid user entries

No entryuserauth_finish() is never called

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.

  1. 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->valid return in userauth_gssapi() is removed entirely — closing the user-validity-oracle signal (§3.2);

  • input_gssapi_errtok() now calls userauth_finish(ssh, 0, "gssapi-with-mic", NULL) instead of returning silently — restoring failures++ / MaxAuthTries accounting and, as a side effect, ending the unlimited GSSSETUP cycling that caused the resource amplification;

  • input_gssapi_errtok() no longer forwards the token to the monitor via mm_ssh_gssapi_accept_ctx() at all — it now only cancels the exchange and logs — which also eliminates the post-GSS_S_COMPLETE monitor state inconsistency, since MONITOR_REQ_GSSSTEP is never reached from this path anymore;

  • a logit() call was added so failed GSSAPI attempts are now visible at the default LogLevel 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