Skip to content

Provider::Multi routes SPNEGO on client-controlled mech ordering with no mechListMIC verification #304

Description

@Pushpenderrathore

Summary

Provider::Multi, added in #303, routes an incoming SPNEGO negotiation to a sub-provider (Kerberos vs. NTLM) using only the mechanism OID named in the client's own NegTokenInit. It does not cross-check that OID against the server's own advertised mechanism set, and no mechListMIC (RFC 4178 §5) is computed or verified anywhere in the GSS stack. The selected mechanism is therefore driven entirely by client-controlled bytes.

This is a follow-up to the routing gap I flagged during review of #303 (#303 (comment)). It was left out of that PR so #303 could land unblocked, per @jheysel-r7's request.

Status: implementation weakness confirmed at the code level; a real-client downgrade and its security impact are not yet demonstrated.

Detail

Multi::Authenticator#process decides routing here:

mech_type = Gss.asn1dig(gss_api, 1, 0, 0, 0, 0)
authenticator = authenticator_for(mech_type)

authenticator_for resolves the OID to the first sub-provider that supports it, so a mechanism the server never advertised is rejected. What is not checked is whether the selected mechanism matches the server's own preference: provider_for walks the client-supplied mechTypeList and honours whichever mechanism the client listed first, rather than the order the server advertised in mech_types.

mech_list_mic is defined in the SPNEGO model (lib/ruby_smb/gss/spnego_neg_token_targ.rb:19) but never read or verified. A repo-wide grep confirms that field and one comment are its only appearances, so there is no integrity check on the negotiated mechanism list.

Evidence

A test run directly against the real Multi::Authenticator#process (not a mock), with a server built to prefer Kerberos and fall back to NTLM, Provider::Multi.new([kerberos_provider, ntlm_provider]):

Client-asserted mechTypeList Mechanism token Routed to
[Kerberos, NTLM] (Kerberos first) Kerberos AP-REQ-shaped token Kerberos
[NTLM] (Kerberos stripped) NTLM Type 1 NTLM
[NTLM, Kerberos] (Kerberos present, reordered second) NTLM Type 1 NTLM

The third row is the point: reordering the list, without removing Kerberos, is enough to flip the server's routing to NTLM. There is no server-side check that would notice.

Confirmed vs. not confirmed

  • Confirmed: server-side routing has no defense against a reordered or tampered mechTypeList. An attacker who can influence the bytes the server receives fully controls which sub-provider handles the request.
  • Not confirmed: that a real Kerberos-capable client (Windows, Samba, MIT krb5) can be induced to send or accept a negotiation of this shape under realistic conditions and then complete NTLM as a result. ruby_smb's own client (client/authentication.rb) only ever sends bare NTLM, so it cannot stand in for a victim client here. An initial real-client lab attempt was inconclusive because of an unrelated test-harness problem; a freshly provisioned Windows client, tested baseline-first, is the way to close this.
  • Unknown: whether the client's own SPNEGO implementation enforces mechListMIC, extended protection, or channel binding that would block this class of downgrade regardless of the server-side gap.

If confirmed: impact

A server built with Provider::Multi.new([kerberos_provider, ntlm_provider]) could be forced by an on-path attacker to authenticate a client over NTLM when both sides support Kerberos, a standard SPNEGO downgrade and the precondition for NTLM relay against a target that would otherwise have preferred Kerberos.

Suggested direction

No fix is proposed yet, so the original behavior is captured here before any change touches it. The likely direction, if the real-client repro warrants it, is to compute and verify a mechListMIC for a multi-mechanism negotiation (an HMAC over the DER-encoded mechTypeList keyed by the negotiated mechanism's session key, per RFC 4178 §5.1) and refuse to complete authentication when it is absent or incorrect. Routing on server preference order alone is not a viable fix on its own, since the client's optimistic mechToken is for a specific mechanism and the server must still route to whichever mechanism that token belongs to.

A routing test that reproduces the table above (security_test_spnego_routing.rb) is in my working tree; I can attach it or fold it into a spec.

Activity

  1. Pushpenderrathore commented on Oct 3, 2026

    @Pushpenderrathore
    ContributorAuthor

    Opened #309 as the proposed fix.

    Addresses the "Confirmed" half here: Multi::Authenticator#process now walks the full mechTypeList, picks the server's most-preferred advertised mechanism, and replies NegTokenResp accept-incomplete / supportedMech per RFC 4178 section 4.2.2 when that choice is not what the client's optimistic mechToken was built for.

    Does not add mechListMIC verification. That stays on this issue as follow-up.

    The "Suggested direction" paragraph above ruled out server-preference routing on the grounds that it would strand the client's optimistic mechToken. The accept-incomplete round-trip is the way around that, and the PR carries a captured Windows SMB client (from a UTM AD lab) resubmitting a Kerberos AP-REQ for the server-selected mechanism exactly as the spec says it should. So that constraint turns out not to hold in practice.

    The security_test_spnego_routing.rb matrix from my working tree is folded into spec/lib/ruby_smb/gss/provider/multi_spec.rb as part of #309.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions