Skip to content

[self-review] #303 Allow a server to offer Kerberos as well as NTLM - #1

Open
Pushpenderrathore wants to merge 9 commits into
masterfrom
feature/kerberos-gss-provider
Open

Pushpenderrathore wants to merge 9 commits into
masterfrom
feature/kerberos-gss-provider

Conversation

@Pushpenderrathore

Copy link
Copy Markdown
Owner

Self-review scratch PR to run Copilot on the fork. Upstream: rapid7#303. Do not merge.

@Pushpenderrathore
Pushpenderrathore requested a lite review from Copilot September 24, 2026 17:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

cdelafuente-r7 and others added 9 commits September 28, 2026 15:46
Initialize server target info with nil and ignore empty client target-info
buffers while preserving validation of malformed nonempty data.

Update session specs to use real credentials instead of mocking
`is_anonymous?`, and add regression coverage for SMB1, SMB2, and DCE/RPC.
The SPNEGO NegTokenInit a server sends to advertise its authentication
mechanisms was built inside the NTLM authenticator, with a mechTypes list
hardcoded to a single OID_NTLMSSP. A comment there noted the limitation:
"this is only NTLMSSP (as opposed to SPNEGO + NTLMSSP)".

Because the token was owned by the NTLM provider, no other mechanism had a
way to contribute to the advertisement, so a server could never offer a
client anything but NTLM.

Move the NegTokenInit construction to Gss.gss_neg_token_init, which takes
the mechTypes to advertise, and add Provider::Base#mech_types so a provider
declares what it handles. NTLM declares OID_NTLMSSP, so the token it emits
is byte identical to the one it built before.

Also define the Kerberos v5 mechanism OIDs, both the RFC 4121 OID and the
legacy Microsoft variant, since clients may offer or select either.

No behaviour change: this only moves ownership of the mechanism list from
the NTLM provider to the providers themselves.
A server held exactly one GSS provider, so it could only ever offer a
client a single authentication mechanism. SPNEGO exists to let the two
sides agree on a mechanism, but with one on offer there is nothing to
negotiate.

Add Provider::Multi, which holds an ordered list of providers, advertises
the mechanisms of all of them, and routes each request to whichever one
understands the mechanism the client selected. A NegTokenInit names the
mechanism, so that is where the routing decision is made; a NegTokenResp
carries no mechanism OID and is treated as a continuation of the exchange
already under way.

Routing happens in the authenticator rather than at the call sites, so it
covers SMB1 and SMB2/3 alike: every request already funnels through
ServerClient#process_gss.

Sub-authenticators are built lazily, so a mechanism that is advertised but
never selected is never instantiated, and the session key of whichever
mechanism actually authenticated is exposed to the server for signing.

Wrapping a single provider produces a byte identical advertisement and an
identical authentication result, so existing servers are unaffected.
A Kerberos AP-REQ is encrypted to the service the client believes it is
talking to, so a server that does not hold that service's key cannot read
it. Provider::Kerberos therefore does not try: it advertises the Kerberos
mechanisms, and hands the mechanism token to a handler that decides how to
reply.

That is enough for a server to observe or forward Kerberos authentication,
and it keeps Kerberos message parsing out of this library, so no new
dependency is introduced and the token is never altered in transit. A
handler receives the bytes exactly as the client sent them, which matters
for anything that forwards the ticket elsewhere.

Both the RFC 4121 mechanism OID and the legacy Microsoft variant are
advertised, since clients may select either, and the RFC 4121 token
identifiers are exposed so a handler can tell an AP-REQ from an AP-REP or
a KRB-ERROR without decoding the payload.

With no handler set the attempt is refused rather than silently accepted,
since nothing here can validate a ticket.

Accepting Kerberos properly, by decrypting the ticket with a service key
and validating the PAC, is a separate concern and is not implemented here.
Lab testing against a Windows domain controller showed the documentation
here was wrong about the shape of the token a client sends.

The mechanism token is a GSS-API InitialContextToken (RFC 2743 section
3.1), which wraps the mechanism OID and the token identifier around the
Kerberos message:

  60 82 0c 0e                 InitialContextToken
    06 09 2a 86 48 ..         the mechanism OID
    01 00                     the token id, here KRB_AP_REQ
    6e 82 0b fd ..            the AP-REQ itself

So the token id follows the OID rather than starting the token, which is
what the previous comment implied, and the framing around it is not valid
ASN.1, so OpenSSL::ASN1.decode cannot read it.

Correct the documentation and add Kerberos.token_id, which locates the
identifier by walking the lengths, so a handler can tell an AP-REQ from an
AP-REP or a KRB-ERROR without decoding the payload or guessing at offsets.

The provider itself was already handing up the token unaltered, which is
what matters for anything forwarding it; only the description of it was
wrong.
Replace the asn1dig chains in the Kerberos GSS provider and the
hand-rolled NegTokenInit builder with RASN1 model types, following the
approach used in metasploit-framework #20967.

Add SpnegoNegTokenInit and SpnegoNegTokenTarg under RubySMB::Gss, along
with a GeneralString type and a NegHints model so the advertisement,
including the Microsoft negHints placeholder, can be built by the model.
The token gss_neg_token_init produces stays byte-identical to the one
the hand-rolled builder produced.

extract_mech_token now parses through those models, dispatching on the
SPNEGO identifier octet, and Gss.asn1dig is kept since the NTLM provider
still relies on it.

Declare rasn1 >= 0.12 (the release that introduced the model wrapper DSL
these types use).
Two review findings on the Kerberos provider:

- Provider::Kerberos::Authenticator#process forwarded whatever
  on_mech_token returned as the GSS result, but the session setup path
  calls nt_status on it, so a handler returning a non-Result (such as the
  boolean a naive handler might return, as the earlier example showed)
  crashed the connection. Refuse anything that is not a Result, and
  correct the on_mech_token example to return one.

- The byte-identity spec only covered the three-mechanism advertisement,
  not the NTLM-only token a default server still emits. Add a lock
  against the exact pre-change NTLM-only bytes so the backwards
  compatible wire format cannot drift.
@Pushpenderrathore
Pushpenderrathore force-pushed the feature/kerberos-gss-provider branch 2 times, most recently from 8464f05 to dde78bc Compare September 30, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants