Add native Kerberos relay stack and ESC8 (AD CS Web Enrollment) target (CVE-2026-20929) - #21709
Add native Kerberos relay stack and ESC8 (AD CS Web Enrollment) target (CVE-2026-20929)#21709Pushpenderrathore wants to merge 19 commits into
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 |
Foundation for native Kerberos relay support. Introduces the Msf::Exploit::Remote::Relay::Kerberos namespace mirroring the NTLM relay stack, with an ApReqExtractor mixin that pulls a captured AP-REQ out of a client's GSS-API token (SPNEGO NegTokenInit or bare GSS Kerberos) as opaque DER, ready to forward to a relay target unchanged. The AP-REQ is carried, never interpreted: the client identity lives in its encrypted ticket/authenticator which only the real target decrypts, and ApReq#decode is not implemented. RelayResult struct added for the target-reply contract. Covered by rspec (round-trip plus NTLM/malformed rejection).
Rename the AP-REQ extractor mixin to GssApReq to reflect that it now handles both directions of the relay: extract_ap_req captures a client's AP-REQ (relay server side), and the new build_spnego_ap_req re-wraps that captured AP-REQ into a fresh GSS-SPNEGO blob to send to the real service (relay target side). build_spnego_ap_req is the inverse of extract_ap_req and takes raw AP-REQ DER rather than an ApReq model object, since the relay only holds the captured bytes. Round-trip specs assert extract -> build -> extract yields the original AP-REQ unchanged.
Adds Relay::Kerberos::Target::HTTP::Client, which replays a captured AP-REQ to a real HTTP service (e.g. AD CS Web Enrollment for ESC8) over a SPNEGO Negotiate exchange. Unlike NTLM there is no challenge/response round-trip: the AP-REQ is a complete credential sent in a single request, and on success the connection is left open for the calling module to issue authenticated follow-up requests. Moves RelayResult under the Target namespace to satisfy Zeitwerk (a target.rb file must define Target); mirrors the NTLM target layout. Verified by rspec (network mocked: Negotiate header contents and success/failure status mapping) and the zeitwerk_compliance spec.
Adds Relay::Kerberos::RelayHandler, the Kerberos counterpart to the NTLM server client's relay_ntlmssp. Given an incoming client GSS token it dispatches on mechanism (Kerberos vs NTLM), extracts the AP-REQ, relays it to the target client, and fires the module's on_relay_success / on_relay_failure and on_relay_end callbacks. The flow is one-shot: a captured AP-REQ is a complete credential, so there is no challenge/response and no per-identity target selection. The AP-REQ is cryptographically bound to the SPN the attacker coerced, so it can only be relayed to the matching service. Non-Kerberos tokens return nil so a shared relay server falls through to its NTLM path. Protocol-agnostic (the RubySMB/HTTP server plumbing lives in the including class); verified by rspec with the target and callbacks mocked.
Adds Target.create_client, the single dispatch point mapping a relay target's protocol to its per-protocol client (HTTP today). Mirrors the NTLM server client's create_relay_client and gives the relay server one call to build a target, with a clear extension point for future protocols (e.g. LDAP). Verified by rspec.
Adds SMB::Relay::Kerberos::ServerClient, the Kerberos counterpart to the NTLM SMB relay server client. A coerced host authenticates over SMB; its SMB2 SessionSetup carries a SPNEGO-wrapped Kerberos AP-REQ. The one-shot flow (no NTLM-style challenge) captures that AP-REQ, dispatches on kerberos_ap_req?, selects the SPN-matching target, builds its client via Target.create_client, and relays through relay_kerberos. The relay decision (target selection + relay) is split into relay_captured_ap_req and unit-tested; the SMB status mapping is tested too. The exact SMB2 SessionSetup response shape is noted as pending live validation against a coerced client. Validated on real lab data: the capture->extract->rebuild->Negotiate->CA(200) forward path was confirmed end-to-end against the live AD CS server.
Adds SMB::Relay::Kerberos::Server, the Kerberos counterpart to the NTLM SMB relay server. Accepts incoming SMB connections from coerced hosts and services each with a Kerberos ServerClient on its own thread, completing the capture side of the relay. Mirrors the NTLM server's dialect set, accept loop, and shutdown; closed?/close behaviour is unit-tested.
Wire the Kerberos relay stack (CVE-2026-20929) through to AD CS ESC8 certificate enrollment: - Make Relay::Kerberos::Target::HTTP::Client drivable as an HTTP client (request_raw/request_cgi/send_recv delegators) so WebEnrollment can reuse the Kerberos-authenticated connection after a successful relay. - Add SMB::Relay::Kerberos::RelayServer, a reusable module-level mixin mirroring SMB::RelayServer that runs the Kerberos SMB relay server and keeps the relay server decoupled from the target action. - Add auxiliary/server/relay/esc8_kerberos, which relays a captured AP-REQ to AD CS Web Enrollment and requests a certificate. Identity is supplied via RELAY_IDENTITY since the AP-REQ carries it encrypted.
The SessionSetup answer skipped three things the NTLM relay server client does, all of which the coerced client depends on. Sessions are now registered. A zero session id mints a new id and stores a RubySMB::Server::Session in the session table, so RubySMB can resolve the session for any follow-up request; previously the generated id was returned to the client but recorded nowhere. A non-zero id that this server never issued is now answered with STATUS_USER_SESSION_DELETED rather than a normal SessionSetup response. Credits are now granted. RubySMB does not add them for us, so a response carrying none leaves the client with no allowance to send anything further and the exchange stalls. One credit is granted up front and 32 on success, matching the NTLM path. On success the session is marked valid. session.key is deliberately left unset and signing is not requested: the AP-REQ is relayed as opaque DER and only the real target service can decrypt it, so we never learn the Kerberos session key and could not sign as the victim. That is harmless because the relay is one-shot, but it does mean the session must not be marked as requiring signing. Adds seven specs covering session registration, id reuse, rejection of an unknown id, the credit grant, the state transition, and the absence of a session key.
00e1d07 to
c9715ff
Compare
|
@jheysel-r7 hit something in lab testing I could use your steer on. Set up a decoy SPN and pointed a real DC at the relay over SMB. It never sends an AP-REQ. Every attempt comes in as NTLM: Took me a while to work out why. RubySMB doesn't have the Kerberos OID at all:
That makes this look like a ruby_smb change rather than something to fix here. Teaching the provider to advertise the Kerberos mech would also let the existing NTLM relay server negotiate it. The alternative is building the negotiate blob myself in the module, but that's duplicating gem internals and I'd rather not go there without asking. So before I open anything against ruby_smb: was the server side only offering NTLM deliberately, or has nobody needed Kerberos there yet? And if the gem is the right place, do you want that PR and the version bump paired with this one? On the plus side the live test did confirm the session handling works, and it shook out a crash along the way: a SPNEGO NegTokenResp raises |
|
@jheysel-r7 follow-up after more lab time on this. Two findings, and I still want your steer on the second. First one, a crash. I set up a decoy SPN, pointed a real DC at the relay over SMB, and every connection died on the second SessionSetup with The second one is what I want your read on. Once it stopped crashing, the DC just authenticated with NTLM, never sent an AP-REQ: Took me a while to work out why. RubySMB doesn't have the Kerberos OID anywhere:
That looks like a ruby_smb change to me rather than something to fix in the module: have the GSS provider advertise the Kerberos mech so a client can select it, which would also let the existing NTLM relay server negotiate Kerberos. The alternative is building the negotiate blob myself in the module, but that's duplicating gem internals and I'd rather not without asking. So two questions before I open anything against ruby_smb: was the server side only offering NTLM on purpose, or has nobody needed Kerberos there yet? And if the gem is the right place, do you want the ruby_smb PR and the version bump handled as a pair with this one? I also tried to validate the coercion modules (the DHCPv6/RA DNS takeover) against the real victim while I was at it. Couldn't get a clean run, and it's worth being upfront about why. My Mac can't host the coercion since mDNSResponder squats on port 53 and it is SIP-protected, so I vendored the module onto the Kali attacker's stock Metasploit instead. The DNS half came up fine ("DNS server started, poisoning names under kerberos.issue"), but the DHCPv6 half kept throwing Full coerce -> relay -> cert is still pending regardless, and to mean much it needs a two-host lab with the CA separate from the KDC (a single host collapses the KDC into the target). |
Parse the GSS blob once. do_session_setup_smb2 tested for Kerberos and then relay_kerberos tested again and extracted, so a single SessionSetup parsed the same blob up to four times. GssApReq gains try_extract_ap_req, which yields the AP-REQ or nil in one pass; kerberos_ap_req? is now defined in terms of it, and relay_kerberos takes the extracted AP-REQ rather than the raw blob. Deciding whether a blob is Kerberos at all, and falling through to NTLM when it is not, now belongs unambiguously to the caller. Fix the RHOSTS aliases. HttpClient re-registers RHOSTS after the relay server mixin and drops its aliases, so SMBHOST and RELAY_TARGETS were accepted at the prompt but never reached RHOSTS: setting either reported success and left the module with no target. Re-registered at module level, which is applied last. Verified that both aliases now set RHOSTS, and that the relay-specific description is the one shown. Keep the closing log line honest when the peer lookup fails. ip_address was assigned inside the begin block but read after the rescue, so a failure in getpeername left it interpolating nil. Also drop @issued_certs, which was assigned and never read, correct a doc reference to Kerberos::Target::RelayResult, and note that relay_identity is always nil on the Kerberos path today and is honoured only for a future target that can recover an identity.
Found in a live lab test against a Windows DC. The relay never survived a real SMB2 SessionSetup exchange. unwrap_pseudo_asn1 only assigns its start offset when it finds a top-level mechanism OID. For a token that has none it leaves the offset nil and then evaluates `token.length - nil`, raising TypeError rather than an ASN1Error. safe_unwrap rescued only ASN1Error, so the TypeError escaped through extract_ap_req and try_extract_ap_req and killed the connection thread. A SPNEGO NegTokenResp is exactly such a token, and a Windows client sends one as the second leg of a SessionSetup. Every connection therefore died mid-exchange with "nil can't be coerced into Integer" after the first message. safe_unwrap now rescues TypeError as well, so a NegTokenResp is reported as "not a Kerberos mechanism" and the caller falls through to the NTLM path as intended. Verified in the lab: the same exchange that previously died now completes, the session reaches :valid and the following TREE_CONNECT succeeds. Also pass the exception to elog as error: rather than as the message, so the backtrace survives. Diagnosing this from the log was impossible without it, since elog(exception) records only the message.
69d94e2 to
2446062
Compare
|
Hey @Pushpenderrathore, thanks for raising all these concerns.
As for the DHCPv6/RA DNS takeover issue, thanks for being upfront - I think that will be a solvable issue. We should be able to test everything on the kali system eventually. |
|
Put it up as a Open so you can look at the actual code rather than my description of it: rapid7/ruby_smb#303. Three commits, 650 lines with specs. Providers declare their own mechanisms, Left as a draft until you've had a chance to weigh in on the scope question above. If capture and forward is the right depth it's ready as it stands, and if you'd rather it went all the way to real acceptance the refactor underneath still holds and I'd build on it rather than start again. |
Both were found running the relay against a live domain controller, with ruby_smb teaching the SMB server to advertise Kerberos so a real client would actually send an AP-REQ. Neither could be reached before, because nothing had ever got past the capture stage. The AP-REQ relayed and AD CS accepted it, then the module raised "undefined method `[]' for nil". HTTP::WebEnrollment#cert_issued? reads @issued_certs on the first certificate request, and only the NTLM ESC8 module was initialising it. With that fixed the enrollment reached the target and raised "undefined method `conn'". The Kerberos HTTP relay client stands in for a Rex::Proto::Http::Client when it is handed to #send_request_raw, and that method reaches for the underlying socket after every response to trace the peer certificate. Delegate #conn so it can. With both fixed the chain completes: a coerced client's AP-REQ is relayed to AD CS Web Enrollment and a client-auth certificate is issued for the coerced principal, which PKINIT then exchanges for a TGT.
|
Ran this against a live DC with rapid7/ruby_smb#303 in place, and the whole chain works: coerced client to relayed AP-REQ to an AD CS certificate to a TGT. It also shook out two bugs in here that nothing could have reached before, since the relay had never actually got past the capture stage. Lab was Server 2022, The interesting thing about combining the two is how little glue it needs. This PR intercepts the SessionSetup blob directly: ap_req = try_extract_ap_req(request.buffer.to_binary_s)
return super if ap_req.nil?so it never asks the GSS provider anything. All #303 has to do is widen the mechanism list in the NEGOTIATE response so the client is allowed to pick Kerberos. Once it can, the existing capture path here just works. The wiring is about ten lines in gss_provider = ::RubySMB::Gss::Provider::Multi.new(
[::RubySMB::Gss::Provider::Kerberos.new, gss_provider]
)I have deliberately not committed that, since it cannot work against the released gem. It is the follow-up once #303 lands and the version is bumped. The two bugsBoth only appear after a relay succeeds, which is why the specs never saw them. The AP-REQ relayed, AD CS accepted it, and then With that fixed it got as far as the enrollment request and raised Both fixed in efbdada, specs still 60/60 and msftidy clean. The full run, end to endQUERY_ONLY, enumerating templates as the relayed identityBefore issuing anything, The issued certificateSigned by the real CA, with a private key, PKINIT with the relayed certificateThe payoff, using the issued certificate to get a TGT: The DC's clock had drifted about two hours mid-session and this failed with Reproducing itand from the victim: Kerberos only gets selected when explicit domain credentials are given. Without Afterwards the SPN was unregistered and the DNS record removed. One caveat in the description I can now dropThe PR says full validation needs a two-host lab with the CA separate from the KDC. That turns out not to be true for this path. The concern was that poisoning the CA's name would cut the victim off from the KDC, but the coercion here introduces a new name rather than poisoning an existing one, so the victim keeps talking to the KDC normally. CA and KDC on the same host was fine. Still outstanding is |
send_request_raw runs a best-effort peer-cert trace after each response by reading c.conn. A relay target client stands in for a Rex::Proto::Http::Client but does not expose a connection object, so c.conn raised "undefined method 'conn'" for Relay::NTLM::Target::HTTP::Client once an NTLM relay authenticated. The existing &. only guarded a nil connection, not a client that has no conn method at all. Guard with respond_to?(:conn) so the trace is skipped for such clients instead of crashing the relay; clients that do expose a connection are unaffected.
|
One note on the red check so it doesn't get misread: the I'll be first assumed this was my #21720 flake showing up again, but the log says otherwise. This one is the linux meterpreter session going quiet during So it's And it isn't this change: meterpreter session commands never touch the peer-cert trace in http_client.rb, and everything else is green, all the Verify rspec runs and SMB acceptance. A re-run should sort it. |
RELAY_IDENTITY was documented as HOST$@realm but the WebEnrollment mixin splits it on '\' to build the CSR subject and pick the certificate template. A UPN string has no backslash, so both halves became the whole value: the request went out with a doubled HOST$@realm\HOST$@realm subject and AUTO mode misread the machine account because it no longer trailed a '$'. That surfaced as "Certificate request denied ... for HOST$@realm\HOST$@realm" in live testing. Normalize the identity to DOMAIN\HOST$ before use, accepting either the DOMAIN\HOST$ or the HOST$@realm form, and correct the documentation and the option description to match. Adds a module spec for the conversion.
Document what a reproducer needs: the domain controller and AD CS build (Server 2022, Web Enrollment, no registry changes), the certificate template and the enroll right the coerced account needs, how the SPN and DNS records for the coerced name are created, how machine-account Kerberos is coerced from a SYSTEM context, and the real show options output for both the relay module and the paired coercion module.
Replace the relay-local GssApReq mixin with the shared Rex::Proto::Gss::KerberosToken added in rapid7#21717, so the Kerberos token parsing lives in rex/proto/gss rather than under relay/. KerberosToken exposes the same extract_ap_req/try_extract_ap_req/kerberos_ap_req?/ build_spnego_ap_req interface as class methods and already rescues the NegTokenResp TypeError, so the capture path, the SPNEGO rebuild and the crash-safety are all preserved.
|
Ran the refactor from the last two commits ( Setup: decoy SPN RELAY_IDENTITY set to the exact UPN form from the bug report ( Issued certificate, verified with openssl: This exercises the |
The illustrative run output didn't match the module's actual log lines (it invented strings like "New Kerberos request" and "Received AP-REQ for coerced principal" that the code never prints). Rebuilt it from the real format strings in relay_handler.rb, cert_request.rb and web_enrollment.rb, so it now matches what the module actually logs for the AD\WIN-VICTIM$ example above it. Also added an unedited capture from a live lab run using the UPN form of RELAY_IDENTITY, plus the issued certificate's subject/issuer/UPN, so the identity-format handling has a real worked example alongside the illustrative one.
There was a problem hiding this comment.
Pull request overview
Adds the first phase of native Kerberos AP-REQ relaying to Metasploit by introducing a Kerberos SMB relay server/client stack and an ESC8 (AD CS Web Enrollment) auxiliary module that reuses the relayed, already-authenticated HTTP connection.
Changes:
- Introduces
relay/kerberosandsmb/relay/kerberoslibrary code to capture a Kerberos AP-REQ from SMB2 SessionSetup and replay it to HTTP(S) viaAuthorization: Negotiate. - Adds
auxiliary/server/relay/esc8_kerberos+ docs to drive AD CS Web Enrollment over the relayed Kerberos-authenticated HTTP connection. - Updates
HttpClient#send_request_rawto avoid assuming all injected clients respond to#conn(enables the relay HTTP client wrapper).
Impact Analysis:
- Blast radius: medium — new relay stack under
lib/msf/.../relay/kerberosplus a coreHttpClientbehavior tweak that can affect any module passing a non-standardclient:intosend_request_raw. - Data and contract effects: low — no schema changes; adds new relay-target client types and relies on
HTTP::Auth/preferred_authbehavior for follow-up requests. - Rollback and test focus: rollback is straightforward (new files + one-line core change); focus testing on module runtime option validation (
HTTP::Auth/ enrollment flow), and the new relay stack specs.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/modules/auxiliary/server/relay/esc8_kerberos_spec.rb | Adds unit coverage for relay-identity normalization used by the ESC8 Kerberos module. |
| spec/lib/msf/core/exploit/remote/smb/relay/kerberos/server_spec.rb | Tests the Kerberos SMB relay server lifecycle/close behavior. |
| spec/lib/msf/core/exploit/remote/smb/relay/kerberos/server_client_spec.rb | Tests Kerberos AP-REQ extraction/relay decision flow and SMB2 SessionSetup responses. |
| spec/lib/msf/core/exploit/remote/smb/relay/kerberos/relay_server_spec.rb | Tests Kerberos relay server mixin service wrapper behavior and option wiring. |
| spec/lib/msf/core/exploit/remote/relay/kerberos/target/http/client_spec.rb | Tests the HTTP target client’s Negotiate header construction and follow-up request reuse. |
| spec/lib/msf/core/exploit/remote/relay/kerberos/target_spec.rb | Tests protocol dispatch for Kerberos relay target client factory. |
| spec/lib/msf/core/exploit/remote/relay/kerberos/relay_handler_spec.rb | Tests protocol-agnostic one-shot Kerberos relay handler success/failure behavior. |
| modules/auxiliary/server/relay/esc8_kerberos.rb | Adds the ESC8 Kerberos relay module (SMB capture → HTTP(S) Web Enrollment). |
| lib/msf/core/exploit/remote/smb/relay/kerberos/server.rb | Adds Kerberos SMB relay server implementation built on RubySMB server. |
| lib/msf/core/exploit/remote/smb/relay/kerberos/server_client.rb | Adds per-connection SMB server client that extracts AP-REQ and relays it one-shot. |
| lib/msf/core/exploit/remote/smb/relay/kerberos/relay_server.rb | Adds module mixin to run the Kerberos SMB relay service and integrate with TargetList. |
| lib/msf/core/exploit/remote/relay/kerberos/target/http/client.rb | Adds HTTP relay target client that wraps AP-REQ as SPNEGO and replays it over HTTP. |
| lib/msf/core/exploit/remote/relay/kerberos/target.rb | Adds Kerberos relay target factory/dispatcher (HTTP/HTTPS supported). |
| lib/msf/core/exploit/remote/relay/kerberos/relay_handler.rb | Adds protocol-agnostic Kerberos relay orchestration logic (one-shot AP-REQ replay). |
| lib/msf/core/exploit/remote/http_client.rb | Makes peer-cert tracing resilient to injected clients that don’t implement #conn. |
| documentation/modules/auxiliary/server/relay/esc8_kerberos.md | Adds end-user documentation and example workflow for ESC8 Kerberos relay usage. |
Suppressed comments (2)
spec/lib/msf/core/exploit/remote/smb/relay/kerberos/relay_server_spec.rb:22
- Suggestion: Problem: This expectation hardcodes a private RFC1918 address (
10.0.0.1) instead of TEST-NET-1 (192.0.2.0/24) for spec IPs (AGENTS.md:25; RSpec Test Instructions). Impact: inconsistent spec data compared to the project’s established convention. Fix: update the expected LocalHost to match the TEST-NET-1 address used in the spec setup.
expect(described_class.sock_options_for(options)).to include(
'LocalHost' => '10.0.0.1',
'LocalPort' => 4445
)
end
spec/lib/msf/core/exploit/remote/smb/relay/kerberos/relay_server_spec.rb:35
- Suggestion: Problem: This expectation hardcodes a private RFC1918 address (
10.0.0.1) instead of a TEST-NET-1 address for spec IPs (AGENTS.md:25; RSpec Test Instructions). Impact: inconsistent spec data compared to the project’s convention. Fix: update the expected alias to match the TEST-NET-1 address used in the spec setup.
describe '.hardcore_alias' do
it 'derives from the bind host and port' do
expect(described_class.hardcore_alias(options)).to eq('10.0.0.14445')
end
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # The relayed connection is already authenticated by the AP-REQ, so | ||
| # follow-up enrollment requests must not attempt to re-authenticate. | ||
| 'DefaultOptions' => { 'HTTP::Auth' => 'None' }, | ||
| 'PassiveActions' => [ 'Relay' ], |
| def validate | ||
| errors = {} | ||
|
|
||
| case datastore['MODE'] |
| let(:options) do | ||
| { | ||
| socket: { 'LocalHost' => '10.0.0.1', 'LocalPort' => 4445 }, | ||
| smb_server: { gss_provider: double('provider'), relay_targets: double('targets') } | ||
| } | ||
| end |
| # -*- coding: binary -*- | ||
|
|
jheysel-r7
left a comment
There was a problem hiding this comment.
Great work @Pushpenderrathore. I was able to get this working with the ipv6_ra_dns_takeover module. If you could please address the co-pilot comments, I'll kick of the additionally required cucumber tests and if they come back looking good I think we'll be almost ready to land.
Testing
msf auxiliary(server/relay/esc8_kerberos) >
[*] Checking endpoint on http://172.16.199.200:80/certsrv/
[*] SMB Kerberos relay server is running. Listening on 0.0.0.0:445
[*] Server started.
[*] New request from 172.16.199.100
I, [2026-08-19T12:49:34.278158 #8081] INFO -- : Starting thread for connection from 172.16.199.100
I, [2026-08-19T12:49:34.295004 #8081] INFO -- : Negotiated dialect: SMB v2.0.2
D, [2026-08-19T12:49:34.300163 #8081] DEBUG -- : Dispatching request to do_session_setup_smb2 (session: nil)
[*] Relaying Kerberos AP-REQ to http://172.16.199.200:80/certsrv/
[+] Successfully relayed Kerberos AP-REQ to http://172.16.199.200:80/certsrv/
[*] Building a certificate signing request for user minion2$ - RSA key size: 2048 - digest algorithm: SHA256 - template: Machine
[*] Submitting the certificate signing request to the target...
[+] Certificate generated using template Machine for KERBEROS\minion2$
[*] Attempting to download the certificate from /certsrv/certnew.cer?ReqID=325&
[*] Certificate Policies:
[*] * clientAuth
[*] * serverAuth
[*] Certificate DNS: minion2.kerberos.issue
[*] Certificate stored at: /home/msfuser/.msf4/loot/20260819124934_default_172.16.199.200_windows.ad.cs_254002.pfx
[*] Relay tasks complete; waiting for next login attempt.
I, [2026-08-19T12:49:34.977707 #8081] INFO -- : Ending thread for connection from 172.16.199.100
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) >
[*] DNS server started, poisoning names under kerberos.issue -> CNAME devbox.kerberos.issue
[*] Advertising fdd6:b808:d74a:dba4::dead:beef as the IPv6 DNS server via Router Advertisements every 30s
[*] Responding to Router Solicitations with an immediate unicast RA
[+] Answered Router Solicitation from 00:0c:29:1f:71:75 (fe80::b333:c90e:ec8e:1c9b) -> RDNSS fdd6:b808:d74a:dba4::dead:beef
[+] Answered Router Solicitation from 00:0c:29:1f:71:75 (fe80::b333:c90e:ec8e:1c9b) -> RDNSS fdd6:b808:d74a:dba4::dead:beef
[+] Answered Router Solicitation from 00:0c:29:1f:71:75 (fe80::b333:c90e:ec8e:1c9b) -> RDNSS fdd6:b808:d74a:dba4::dead:beef
[+] Answered Router Solicitation from 00:0c:29:e4:20:b1 (fe80::f9fa:d3a7:9985:2172) -> RDNSS fdd6:b808:d74a:dba4::dead:beef
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > [+] Answered Router Solicitation from 00:0c:29:e4:20:b1 (fe80::f9fa:d3a7:9985:2172) -> RDNSS fdd6:b808:d74a:dba4::dead:beef
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) >
And verifying the cert received works correctly:
msf auxiliary(admin/kerberos/get_ticket) > run
[*] Running module against 172.16.199.200
[*] 172.16.199.200:88 - Getting TGT for [email protected]
[+] 172.16.199.200:88 - Received a valid TGT-Response
[*] 172.16.199.200:88 - TGT MIT Credential Cache ticket saved to /home/msfuser/.msf4/loot/20260819154958_default_172.16.199.200_mit.kerberos.cca_785404.bin
[*] Auxiliary module execution completed
msf auxiliary(admin/kerberos/get_ticket) >
|
Additional test pipeline started ⌛ |
|
Slice summary:
Note: build results only accessible to maintainers. |
Part of #21693.
First of a stacked series adding native Kerberos authentication relay to Metasploit. This PR contributes the Kerberos relay stack and the AD CS Web Enrollment (ESC8) target; the DHCPv6 and rogue-RA coercion modules follow in separate PRs.
What this adds
Metasploit's relay stack is NTLM-only today. This mirrors the NTLM namespace under
relay/kerberosandsmb/relay/kerberos:create_clienttarget factory feeds an HTTP target client that replays the AP-REQ overAuthorization: Negotiate.modules/auxiliary/server/relay/esc8_kerberos.rbdrives the existingHTTP::WebEnrollmentmixin over the relayed, already-authenticated connection to enroll a certificate as the coerced principal.Unlike NTLM, an AP-REQ is a single self-contained message with no challenge/response, so the relay is one-shot with no per-identity target selection.
Background
CVE-2026-20929 (GHSA-cjjj-mhw7-f4xr; Cymulate, "Kerberos relay via DNS CNAME abuse") is catalogued as improper access control in Windows HTTP.sys (CWE-284, CVSS 7.5). The January 2026 patch added channel binding to HTTP.sys but does not cover targets that do not enforce it, such as plain-HTTP ESC8
/certsrv, which has no TLS channel to bind and is the primary target here.Validation
Lab-validated end to end against a live domain: a coerced Windows client's SMB2 AP-REQ is captured, round-tripped byte-identical through the GSS code, rebuilt into a
Negotiateheader, and relayed to AD CS Web Enrollment to issue a certificate for the coerced principal with no knowledge of its credentials. That certificate then drives PKINIT to obtain a valid TGT for the principal, completing the coerce to relay to certificate to TGT chain.Capturing the AP-REQ from a real client depends on ruby_smb advertising the Kerberos mechanism in its SMB negotiate, which is the paired change in rapid7/ruby_smb#303. The small provider glue that turns it on here is applied once that lands and the gem is bumped.
Correction to an earlier note in this description: the full chain does not need a two-host CA != KDC lab. The coercion introduces a new name rather than poisoning an existing one, so the victim keeps reaching the KDC, and a single host with the CA and KDC co-located is sufficient.
Notes
relay/kerberos+smb/relay/kerberos) pass; msftidy clean on the module.