Skip to content

Verify the client's mechListMIC on the final SPNEGO leg - #310

Open
Pushpenderrathore wants to merge 2 commits into
rapid7:masterfrom
Pushpenderrathore:fix/multi-provider-mech-list-mic
Open

Pushpenderrathore wants to merge 2 commits into
rapid7:masterfrom
Pushpenderrathore:fix/multi-provider-mech-list-mic

Conversation

@Pushpenderrathore

@Pushpenderrathore Pushpenderrathore commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

The remaining half of #304. #309 moved the server to pick its own preferred mechanism out of the client's mechTypeList, which closes the reorder gap, but a mechListMIC check is still needed to catch an on-path attacker who drops a mechanism from the list before the server sees it. This PR adds that verifier.

Provider::Multi::Authenticator now remembers the DER-encoded mechTypeList from the first NegTokenInit. When the client attaches a mechListMIC to its final NegTokenResp the server computes HMAC-MD5 under the NTLM client-to-server signing key derived from the negotiated session key and constant-time compares it to the client's MIC. If the comparison fails, auth returns STATUS_LOGON_FAILURE.

Per [MS-SPNG] section 3.2.5.5 the MIC is REQUIRED on the mismatch path #309 introduced (server-chosen mechanism differs from the client's first-listed one). On the single-mechanism / no-mismatch path RFC 4178 keeps the MIC OPTIONAL, so the server only verifies when the client sent one.

Depends on #309 - this branch is stacked on it.

Scope

  • NTLM session keys only. A Kerberos-negotiated session takes the existing on_mech_token handler path and the MIC check is skipped, because the Kerberos sub-provider does not hold the service key needed to derive a verifier. That is a separate hardening.
  • Verifies the client's MIC, does not emit a server MIC. Clients that insist on receiving a server-side mechListMIC are a follow-up.
  • Covers the DROP attack when the client is spec-compliant. A client that is MS-SPNG conformant (Windows is) signs over its own view of the mechTypeList. An attacker who removes OIDs between the client and the server breaks the HMAC. A client that omits the MIC entirely on the single-mech path still slips past (as RFC 4178 permits); server-side policy of "always require MIC in multi-mech mode" is a separate PR if someone wants it.

Lab PoC

Scripted NTLM client + authenticator wired up to five scenarios. The baseline column swaps lib/ruby_smb/gss/provider/multi.rb to #309's version (no verifier). Everything else identical.

Baseline (#309 base) This branch
A - happy, no MIC SUCCESS SUCCESS
B - drop attack, client sends NO MIC SUCCESS (attack wins) SUCCESS (MIC was OPTIONAL here per RFC 4178)
C - drop attack, client sends MIC over ORIGINAL list SUCCESS (attack wins) FAIL (attack caught)
D - happy, correct MIC over [NTLM] SUCCESS SUCCESS
E - happy, tampered MIC SUCCESS (not verified) FAIL (tampering caught)

C is the real target: a spec-conformant Windows client in a drop-attack scenario produces exactly these bytes, and the verifier catches it.

Verification Steps

  • bundle exec rspec spec/lib/ruby_smb/gss/provider/multi_spec.rb passes with the four new MIC tests
  • Lab PoC produces the table above when run against the two branches
  • Existing routing / continuation / NTLM-exchange specs in this file continue to pass unchanged

Test Evidence

New describe block the client's mechListMIC on the final leg with four cases:

  • accepts a valid MIC on the single-mechanism (no-mismatch) path
  • rejects a MIC that was computed over a different mechTypeList
  • rejects a MIC with one bit flipped
  • leaves the single-mechanism path alone when no MIC is sent (RFC 4178 marks it OPTIONAL)

Related

Provider::Multi::Authenticator#process read only the client's first-listed
mechanism when routing a NegTokenInit. That handed routing to the client:
reordering a mechTypeList, without removing any mechanism, was enough to
steer the server to a weaker sub-provider (NTLM when both sides supported
Kerberos). The gap was tracked in rapid7#304.

Walk the client's full mechTypeList, pick the server's most-preferred
advertised mechanism that the client also offers, and route to that. When
the server's choice is not the mechanism the client's optimistic
mechToken is for, reply with a NegTokenResp carrying accept-incomplete
and supportedMech, per RFC 4178 section 4.2.2, so the client resends a
token for the server-selected mechanism.

This is server-side routing hardening, not a replacement for a mechListMIC
verifier. An on-path attacker who rewrites the mechTypeList before signing
is in effect is still able to drop mechanisms the client offered; stopping
that requires computing and verifying the mechListMIC, which is tracked
separately on the issue.
The last open piece of rapid7#304. Provider::Multi now remembers the DER-encoded
mechTypeList the client sent in its first NegTokenInit, and when the client
attaches a mechListMIC to its final NegTokenResp the server verifies the
HMAC using the negotiated NTLM session key before accepting the auth.

The MIC is REQUIRED (per MS-SPNG section 3.2.5.5) on the mismatch path that
the preceding change introduced: when the server replied accept-incomplete
because its preferred mechanism was not the client's first-listed one. On
the single-mechanism path the MIC remains OPTIONAL per RFC 4178 and is only
verified when present.

Scope:

- Only NTLM session keys are used for verification. Kerberos MIC verification
  needs the service's long-term key, which the Kerberos sub-provider does not
  hold, so a Kerberos-negotiated session takes the existing on_mech_token
  path and skips the MIC check.
- The server does not emit its own mechListMIC back to the client. Clients
  that require one from the server are a follow-up.

Catches the standard SPNEGO drop attack when the client is spec-compliant:
on-path attacker strips Kerberos from the client's mechTypeList, the server
sees only NTLM and routes to NTLM, but the client's MIC was computed over
its original [Kerberos, NTLM] view. The server's expected MIC over [NTLM]
does not match the client's MIC, so authentication fails.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant