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.
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 ownNegTokenInit. It does not cross-check that OID against the server's own advertised mechanism set, and nomechListMIC(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#processdecides routing here:authenticator_forresolves 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_forwalks the client-suppliedmechTypeListand honours whichever mechanism the client listed first, rather than the order the server advertised inmech_types.mech_list_micis 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]):[Kerberos, NTLM](Kerberos first)[NTLM](Kerberos stripped)[NTLM, Kerberos](Kerberos present, reordered second)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
mechTypeList. An attacker who can influence the bytes the server receives fully controls which sub-provider handles the request.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.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
mechListMICfor a multi-mechanism negotiation (an HMAC over the DER-encodedmechTypeListkeyed 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 optimisticmechTokenis 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.