Add shared Kerberos GSS and SPNEGO token handling - #21717
Conversation
|
Thanks for your pull request! As part of our landing process, we manually verify that all modules work as expected. We've added the |
There was a problem hiding this comment.
Pull request overview
This PR adds a shared low-level Rex::Proto::Gss::KerberosToken implementation for parsing/building Kerberos mechanism tokens carried by GSS-API and SPNEGO, and updates existing Kerberos client helpers to delegate to it while normalizing ASN.1 parsing failures.
Changes:
- Introduces
Rex::Proto::Gss::KerberosTokenwith APIs for parsing GSS-Kerberos / SPNEGO NegTokenInit/Resp metadata and extracting/rebuilding opaque AP-REQ payloads. - Normalizes
unwrap_pseudo_asn1failures into anOpenSSL::ASN1::ASN1Errorwhen no top-level mechanism OID exists (avoiding leakingTypeError). - Refactors Kerberos client AP-REQ GSS/SPNEGO encoding helpers and token ID constants to use the shared token implementation.
Impact Analysis:
- Blast radius: medium — impacts callers of
Rex::Proto::Gss::Asn1#unwrap_pseudo_asn1and all consumers of Kerberos client GSS/SPNEGO AP-REQ encoding; downstream relay/tracing consumers are Unknown from diff alone. - Data and contract effects: error behavior changes from potential
TypeErrorleakage to consistent ASN.1/ParseErrorfailure modes; new parsing/building APIs are introduced but existing public entrypoints are retained via delegation. - Rollback and test focus: rollback is straightforward (new class + delegation), but validate SPNEGO parsing edge cases (empty mech list / empty mech token), and confirm Kerberos client authentication flows still produce byte-identical AP-REQ wrappers.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| spec/lib/rex/proto/gss/kerberos_token_spec.rb | Adds RSpec coverage for GSS/SPNEGO parsing, AP-REQ extraction, and builders. |
| spec/lib/rex/proto/gss/asn1_spec.rb | Adds regression coverage for unwrap_pseudo_asn1 raising ASN.1 errors instead of leaking TypeError. |
| lib/rex/proto/gss/kerberos_token.rb | New shared parser/builder for Kerberos tokens inside GSS-API and SPNEGO. |
| lib/rex/proto/gss/asn1.rb | Updates pseudo-ASN.1 unwrap to raise a consistent ASN.1 error when no mechanism OID exists. |
| lib/rex/proto/gss.rb | Ensures required dependencies are loaded for OID constants. |
| lib/msf/core/exploit/remote/kerberos/client/ap_request.rb | Delegates Kerberos AP-REQ GSS/SPNEGO wrapping to KerberosToken. |
| lib/msf/core/exploit/remote/kerberos/client.rb | Reuses shared Kerberos token ID constants from KerberosToken. |
Comments suppressed due to low confidence (1)
lib/rex/proto/gss/kerberos_token.rb:137
- Important: Problem:
extract_ap_reqonly checksmech_tokenfornil, so a present-but-empty SPNEGO mechanism token falls through intoparseand yields a generic parse failure rather than the explicit "empty SPNEGO mechanism token" rejection described in the PR. Impact: callers lose a clear, normalized error cause for a common malformed-input case. Fix: add an explicit empty-string check and raiseParseErrorwith a specific message.
mech_token = spnego[:mech_token]
raise ParseError, 'SPNEGO NegTokenInit does not contain a mechanism token' if mech_token.nil?
|
Additional test pipeline started ⌛ |
|
Slice summary: No test slices found. Note: build results only accessible to maintainers. |
d2adc3b to
1f2d8c6
Compare
|
Hi @eve0805, this is a really nice consolidation. Since #21709 is meant to delegate its AP-REQ handling to this, I took I captured a genuine service ticket the way the relay would: a Server 2022 DC in Feeding that real token through both implementations, everything lines up byte for byte, and each one reads what the other builds: Cross-check results (12/12)Your Two things worth calling out from this: The One heads-up for the delegation, on my side rather than yours: the relay dispatch ( Cross-check harnessRun from the framework root with $LOAD_PATH.unshift(File.expand_path('lib', Dir.pwd))
require 'openssl'
require 'rasn1'
module Rex; module Proto; module Gss; end; end; end
require 'rex/proto/gss'
# my #21709 relay mixin
load File.expand_path('lib/msf/core/exploit/remote/relay/kerberos/gss_ap_req.rb', Dir.pwd)
klass = Class.new { include Msf::Exploit::Remote::Relay::Kerberos::GssApReq }
g = klass.new
KT = Rex::Proto::Gss::KerberosToken
bare = File.binread('raw_mech_token.bin') # real bare GSS-Kerberos InitialContextToken
apreq = File.binread('captured_ap_req.bin') # the AP-REQ inside it
def show(label, ok); puts format(' %-58s %s', label, ok ? 'PASS' : '*** FAIL ***'); end
puts "REAL captured data: bare token #{bare.bytesize}B, AP-REQ #{apreq.bytesize}B, starts #{apreq[0, 4].unpack1('H*')}"
puts
puts '1) extract the AP-REQ from the real token'
kt_x = KT.extract_ap_req(bare)
g_x = g.extract_ap_req(bare)
show('KerberosToken.extract_ap_req == captured AP-REQ', kt_x == apreq)
show('GssApReq.extract_ap_req == captured AP-REQ', g_x == apreq)
show('the two implementations agree byte-for-byte', kt_x == g_x)
show('KerberosToken.kerberos_ap_req? true', KT.kerberos_ap_req?(bare) == true)
show('GssApReq.kerberos_ap_req? true', g.kerberos_ap_req?(bare) == true)
puts
puts '2) rebuild the AP-REQ into a SPNEGO token, cross-read it with the other impl'
kt_blob = KT.build_spnego_ap_req(apreq)
g_blob = g.build_spnego_ap_req(apreq)
show('GssApReq reads what KerberosToken built', g.extract_ap_req(kt_blob) == apreq)
show('KerberosToken reads what GssApReq built', KT.extract_ap_req(g_blob) == apreq)
puts
puts '3) SPNEGO NegTokenInit path on the real token'
init_blob = KT.build_spnego_init(bare)
show('KerberosToken.extract_ap_req from NegTokenInit', KT.extract_ap_req(init_blob) == apreq)
show('GssApReq.extract_ap_req from NegTokenInit', g.extract_ap_req(init_blob) == apreq)
puts
puts '4) the unwrap_pseudo_asn1 TypeError case: a NegTokenResp (2nd SessionSetup leg)'
negresp = OpenSSL::ASN1::ASN1Data.new([
OpenSSL::ASN1::Sequence.new([
OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::OctetString.new('continuation')], 2, :CONTEXT_SPECIFIC)
])
], 1, :CONTEXT_SPECIFIC).to_der
show('KerberosToken.try_extract_ap_req -> nil, no TypeError', KT.try_extract_ap_req(negresp).nil?)
show('GssApReq.try_extract_ap_req -> nil, no TypeError', g.try_extract_ap_req(negresp).nil?)One note on the two red checks, so they are not mistaken for this change. They are two separate jobs, and each one is red because of a single flaky meterpreter acceptance example with the session dropping mid-run, not the GSS parsing: Both suites pass on their non-macOS runners ( Happy to fold the delegation into #21709 once this lands. Thanks for pulling this out into a shared spot. |
|
Additional test pipeline started ⌛ |
|
Slice summary:
Note: build results only accessible to maintainers. |
jheysel-r7
left a comment
There was a problem hiding this comment.
Hey @eve0805, this looks great, thanks for the PR. I've stepped through smb authentication using kerberos and ensured this is working as expected and maintains backwards compatibility.
msf auxiliary(scanner/smb/smb_login) > run
[*] 172.16.199.200:445 - Starting SMB login bruteforce
[*] 172.16.199.200:445 - Loaded a credential from ticket file: /Users/jheysel/.msf4/loot/20260811151219_default_172.16.199.200_mit.kerberos.cca_943325.bin
[+] 172.16.199.200:445 - Success: 'kerberos.issue\Administrator:' Administrator
[*] 172.16.199.200:445 - Scanned 1 of 1 hosts (100% complete)
[*] 172.16.199.200:445 - Bruteforce completed, 1 credential was successful.
[*] 172.16.199.200:445 - You can open an SMB session with these credentials and CreateSession set to true
[*] Auxiliary module execution completed
msf auxiliary(scanner/smb/smb_login) >
Rspecs are also all passing on my end as well as the jenkins pro tests
➜ metasploit-framework git:(1f2d8c6e4f6) ✗ bundle exec rspec spec/lib/rex/proto/gss/
Overriding user environment variable 'OPENSSL_CONF' to enable legacy functions.
Run options:
include {:focus=>true}
exclude {:acceptance=>true}
All examples were filtered out; ignoring {:focus=>true}
Randomized with seed 3527
Rex::Proto::Gss::KerberosToken ...........................
Rex::Proto::Gss::ChannelBinding ............
Rex::Proto::Gss::Asn1 ...
Rex::Proto::Gss::SpnegoNegTokenInit .
Rex::Proto::Gss::SpnegoNegTokenTarg .
Acceptance::DatastoreFormatting .......
Rex::Proto::Gss::Kerberos::MessageEncryptor .......
Top 10 slowest examples (0.28817 seconds, 17.8% of total time):
Rex::Proto::Gss::Kerberos::MessageEncryptor When we are the initiator Is reversible
0.05943 seconds ./spec/lib/rex/proto/gss/kerberos/message_encryptor_spec.rb:52
Rex::Proto::Gss::Kerberos::MessageEncryptor When we are the acceptor Decrypts a real value
0.04074 seconds ./spec/lib/rex/proto/gss/kerberos/message_encryptor_spec.rb:72
Rex::Proto::Gss::Kerberos::MessageEncryptor When we are the acceptor without using subkey The header is correct
0.0383 seconds ./spec/lib/rex/proto/gss/kerberos/message_encryptor_spec.rb:89
Rex::Proto::Gss::KerberosToken.parse rejects a non-Kerberos mechanism OID
0.03293 seconds ./spec/lib/rex/proto/gss/kerberos_token_spec.rb:62
Rex::Proto::Gss::KerberosToken.parse_spnego_init preserves the initiator mechanism preference order
0.02884 seconds ./spec/lib/rex/proto/gss/kerberos_token_spec.rb:105
Rex::Proto::Gss::ChannelBinding.create #digest_algorithm should be SHA256
0.01884 seconds ./spec/lib/rex/proto/gss/channel_binding_spec.rb:79
Rex::Proto::Gss::KerberosToken.parse_spnego_init rejects an empty mechanism list
0.01745 seconds ./spec/lib/rex/proto/gss/kerberos_token_spec.rb:118
Rex::Proto::Gss::KerberosToken.parse_spnego_response normalizes malformed input into ParseError
0.01731 seconds ./spec/lib/rex/proto/gss/kerberos_token_spec.rb:143
Rex::Proto::Gss::KerberosToken.extract_ap_req rejects an empty AP-REQ payload
0.01721 seconds ./spec/lib/rex/proto/gss/kerberos_token_spec.rb:194
Rex::Proto::Gss::KerberosToken.kerberos_ap_req? distinguishes Kerberos AP-REQ tokens from other input
0.0171 seconds ./spec/lib/rex/proto/gss/kerberos_token_spec.rb:219
Top 7 slowest example groups:
Rex::Proto::Gss::SpnegoNegTokenInit
0.0539 seconds average (0.0539 seconds / 1 example) ./spec/lib/rex/proto/gss/spnego_neg_token_init_spec.rb:4
Rex::Proto::Gss::SpnegoNegTokenTarg
0.04359 seconds average (0.04359 seconds / 1 example) ./spec/lib/rex/proto/gss/spnego_neg_token_targ_spec.rb:4
Rex::Proto::Gss::KerberosToken
0.03412 seconds average (0.92118 seconds / 27 examples) ./spec/lib/rex/proto/gss/kerberos_token_spec.rb:6
Rex::Proto::Gss::Kerberos::MessageEncryptor
0.03243 seconds average (0.227 seconds / 7 examples) ./spec/lib/rex/proto/gss/kerberos/message_encryptor_spec.rb:5
Rex::Proto::Gss::Asn1
0.02711 seconds average (0.08134 seconds / 3 examples) ./spec/lib/rex/proto/gss/asn1_spec.rb:6
Rex::Proto::Gss::ChannelBinding
0.02062 seconds average (0.24739 seconds / 12 examples) ./spec/lib/rex/proto/gss/channel_binding_spec.rb:5
Acceptance::DatastoreFormatting
0.00497 seconds average (0.03482 seconds / 7 examples) ./spec/support/acceptance/datastore_formatting_spec.rb:6
Finished in 1.62 seconds (files took 16.45 seconds to load)
58 examples, 0 failures
Randomized with seed 3527
Coverage report generated for RSpec to /Users/jheysel/rapid7/metasploit-framework/coverage.
Line Coverage: 21.42% (3003 / 14019)
Summary
This introduces
Rex::Proto::Gss::KerberosTokenas a shared low-level implementation for handling Kerberos tokens carried by GSS-API and SPNEGO.The new component supports:
NegTokenInitandNegTokenRespmetadata;extract_ap_req, non-raisingtry_extract_ap_req, andkerberos_ap_req?APIs;NegTokenInittokens containing a Kerberos AP-REQ;The existing Kerberos client AP-REQ encoding helpers now delegate to this shared implementation.
Error handling
Malformed or unsupported tokens are normalized into
Rex::Proto::Gss::KerberosToken::ParseError.The parser explicitly rejects:
This also fixes
unwrap_pseudo_asn1leaking aTypeErrorwhen a token does not contain a top-level mechanism OID. It now raises an ASN.1 parsing error that callers can handle consistently.Motivation
Kerberos tracing and the Kerberos relay implementation in #21709 both need the same low-level GSS/SPNEGO operations:
Placing these operations under
Rex::Proto::Gssavoids coupling protocol parsing to either tracing or relay-specific behavior.The relay-specific
GssApReqmixin from #21709 can delegate to this component in a follow-up change. This PR intentionally does not modify the relay implementation itself.Implementation notes
The AP-REQ payload is deliberately treated as opaque DER. It is never decoded or modified, which allows a captured AP-REQ to be extracted and rebuilt without changing the encrypted ticket or authenticator.
For
NegTokenInit, the first offered mechanism is exposed aspreferred_mechrather thanselected_mech, because the acceptor has not selected a mechanism at that stage.Testing
The test coverage includes:
NegTokenInitandNegTokenRespparsing;Validation results: