Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
4f30bf5
Add SSLPeerCertTrace mixin and HTTP peer certificate hook
Pushpenderrathore Jun 23, 2026
b989a56
Consolidate SSLPeerCertTrace into CertificateTrace per jheysel feedback
Pushpenderrathore Jun 25, 2026
28e9929
Add SSL peer cert tracing to LDAP mixin (Phase 2)
Pushpenderrathore Jun 25, 2026
dd05b10
Add SSL peer cert tracing to RDP mixin (Phase 2)
Pushpenderrathore Jun 25, 2026
545569c
Add SSL peer cert tracing to Postgres mixin (Phase 3)
Pushpenderrathore Jun 25, 2026
12fc385
Add full cert chain display in CertificateTrace full mode (Phase 4)
Pushpenderrathore Jun 25, 2026
760b771
Guard postgres peer cert call with respond_to? for plain TCP connections
Pushpenderrathore Jun 25, 2026
e4ce5cc
Fix _ldap_trace_peer_cert calling rhost/rport in LoginScanner context
Pushpenderrathore Jun 25, 2026
91f36ce
Fix _ldap_trace_peer_cert calling rhost/rport in LoginScanner context
Pushpenderrathore Jun 25, 2026
8fee1fb
Add invalid-cert coverage to certificate_peer_cert_trace spec
Pushpenderrathore Jun 25, 2026
4bb795c
Merge branch 'feature/ssl-peer-cert-trace' into feature/ssl-peer-cert…
Pushpenderrathore Jun 25, 2026
23765d8
Merge phase2 fix and add invalid-cert peer cert trace spec coverage
Pushpenderrathore Jun 25, 2026
7f6d3eb
Merge branch 'master' into feature/ssl-peer-cert-trace-phase3
Pushpenderrathore Jul 9, 2026
7b529ca
Label peer certs distinctly from issued certs and CSRs
Pushpenderrathore Jul 9, 2026
ef96c4b
Render chain certs via presenter label instead of string surgery
Pushpenderrathore Jul 9, 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
60 changes: 60 additions & 0 deletions lib/msf/core/exploit/remote/certificate_trace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,66 @@ def certificate_trace(cert)
print_line(certificate_trace_colorize(output))
end

# Surfaces the server TLS peer certificate after a TLS handshake. Uses
# +CertificateTrace+ and +CertificateTraceColors+ so peer cert output is
# consistent with issued-certificate and CSR output. Deduplicates per
# host:port using the database when active, falling back to in-memory dedup
# within a single module run when the database is not connected.
#
# @param cert [OpenSSL::X509::Certificate, String] peer certificate or DER bytes
# @param host [String] remote hostname or IP
# @param port [Integer] remote port
# @return [void]
def certificate_peer_cert_trace(cert, host, port, chain: nil)
return unless certificate_trace_enabled?
return if cert.nil?

@_peer_cert_seen ||= {}
key = "#{host}:#{port}"
return if @_peer_cert_seen[key]

if respond_to?(:framework) && framework&.db&.active
already_noted = framework.db.notes(ntype: 'ssl.peer_cert_seen').any? do |n|
n.host&.address == host && n.service&.port == port
end
if already_noted
@_peer_cert_seen[key] = true
return
end
framework.db.report_note(
host: host, port: port, proto: 'tcp',
ntype: 'ssl.peer_cert_seen',
data: {},
update: :unique
)
end

@_peer_cert_seen[key] = true

mode = datastore['CertificateTrace']
presenter = Msf::Trace::CertificateTracePresenter.new(cert)
# Label peer certs distinctly from issued (x.509) and CSR output so a
# server's TLS cert is not confused with a client/issued certificate.
output = mode == 'full' ? presenter.to_s_full(label: 'Peer Cert') : presenter.to_s_metadata(label: 'Peer Cert')
return unless output

print_line(certificate_trace_colorize(output))

return unless mode == 'full'

# When chain is provided, print each intermediate / root CA cert that the
# server included in the handshake. chain[0] is the leaf (already printed),
# so we start from chain[1].
chain_certs = Array(chain).drop(1)
chain_certs.each_with_index do |chain_cert, idx|
chain_presenter = Msf::Trace::CertificateTracePresenter.new(chain_cert)
chain_output = chain_presenter.to_s_full(label: "Chain #{idx + 1}/#{chain_certs.length}")
next unless chain_output

print_line(certificate_trace_colorize(chain_output))
end
end

# Dispatches a certificate signing request (CSR) trace at the configured
# verbosity level, mirroring #certificate_trace for the request side of an
# enrollment (e.g. AD CS / MS-ICPR). Intended to be called with the CSR and
Expand Down
9 changes: 9 additions & 0 deletions lib/msf/core/exploit/remote/http_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module Exploit::Remote::HttpClient
include Msf::Auxiliary::LoginScanner
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
include Msf::Exploit::Remote::CertificateTrace

#
# Initializes an exploit module that exploits a vulnerability in an HTTP
Expand Down Expand Up @@ -422,6 +423,14 @@ def send_request_raw(opts = {}, timeout = 20, disconnect = false)

res = c.send_recv(r, actual_timeout)

if 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)
certificate_peer_cert_trace(raw_cert, opts['rhost'] || rhost, (opts['rport'] || rport).to_i, chain: raw_chain)
end
end

disconnect(c) if disconnect

res
Expand Down
30 changes: 28 additions & 2 deletions lib/msf/core/exploit/remote/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module Msf
module Exploit::Remote::LDAP
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
include Msf::Exploit::Remote::CertificateTrace
include Metasploit::Framework::LDAP::Client
include Msf::Exploit::Remote::CertificateTrace

Expand Down Expand Up @@ -162,9 +163,15 @@ def ldap_connect(opts = {}, &block)
def ldap_open(connect_opts, keep_open: false, &block)
opts = resolve_connect_opts(connect_opts)
if keep_open
Rex::Proto::LDAP::Client._open(opts, &block)
ldap = Rex::Proto::LDAP::Client._open(opts)
_ldap_trace_peer_cert(ldap.socket)
ldap
else
Rex::Proto::LDAP::Client.open(opts, &block)
Rex::Proto::LDAP::Client.open(opts) do |ldap|
conn = ldap.instance_variable_get(:@open_connection)
_ldap_trace_peer_cert(conn&.socket)
block.call(ldap)
end
end
end

Expand Down Expand Up @@ -358,5 +365,24 @@ def validate_query_result!(query_result, filter=nil)
def ldap_escape_filter(string)
Net::LDAP::Filter.escape(string)
end

private

# Traces the TLS peer certificate on an LDAP socket when CertificateTrace
# is enabled. Delegates to #certificate_peer_cert_trace so dedup and
# formatting are handled consistently with the HTTP path.
#
# @param socket [Rex::Socket, nil] the LDAP socket after TLS handshake
# @return [void]
def _ldap_trace_peer_cert(socket)
return unless certificate_trace_enabled?
return unless socket&.respond_to?(:peer_cert)

raw_cert = socket.peer_cert
return unless raw_cert

chain = socket.peer_cert_chain if socket.respond_to?(:peer_cert_chain)
certificate_peer_cert_trace(raw_cert, rhost, rport.to_i, chain: chain)
end
end
end
11 changes: 10 additions & 1 deletion lib/msf/core/exploit/remote/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Exploit::Remote::Postgres
require 'metasploit/framework/tcp/client'
include Msf::Db::PostgresPR
include Exploit::Remote::Tcp
include Msf::Exploit::Remote::CertificateTrace

# @!attribute [rw] postgres_conn
# @return [::Msf::Db::PostgresPR::Connection]
Expand Down Expand Up @@ -97,6 +98,7 @@ def postgres_login(opts={})
ip = opts[:server] || datastore['RHOST']
port = opts[:port] || datastore['RPORT']
proxies = opts[:proxies] || datastore['Proxies']
ssl = opts.key?(:ssl) ? opts[:ssl] : datastore['SSL']
uri = "tcp://#{ip}:#{port}"

if Rex::Socket.is_ipv6?(ip)
Expand All @@ -105,7 +107,7 @@ def postgres_login(opts={})

verbose = opts[:verbose] || datastore['VERBOSE']
begin
self.postgres_conn = Connection.new(db,username,password,uri,proxies)
self.postgres_conn = Connection.new(db, username, password, uri, proxies, ssl)
rescue RuntimeError => e
case e.to_s.split("\t")[1]
when "C3D000"
Expand All @@ -124,6 +126,13 @@ def postgres_login(opts={})
end
if self.postgres_conn
print_good "#{self.postgres_conn.peerhost}:#{self.postgres_conn.peerport} Postgres - Logged in to '#{db}' with '#{username}':'#{password}'" if verbose
pg_socket = postgres_conn.conn
if pg_socket&.respond_to?(:peer_cert)
certificate_peer_cert_trace(
pg_socket.peer_cert, ip, port.to_i,
chain: pg_socket.respond_to?(:peer_cert_chain) ? pg_socket.peer_cert_chain : nil
)
end
return :connected
end
end
Expand Down
3 changes: 3 additions & 0 deletions lib/msf/core/exploit/remote/rdp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module Msf
module Exploit::Remote::RDP
require 'rc4'
include Msf::Exploit::Remote::Tcp
include Msf::Exploit::Remote::CertificateTrace

#
# Creates an instance of a RDP exploit module.
Expand Down Expand Up @@ -1181,6 +1182,8 @@ def swap_sock_plain_to_ssl
raise
end

certificate_peer_cert_trace(ssl.peer_cert, rhost, rport.to_i, chain: ssl.peer_cert_chain)

self.rdp_sock.extend(Rex::Socket::SslTcp)
self.rdp_sock.sslsock = ssl
self.rdp_sock.sslctx = ctx
Expand Down
11 changes: 7 additions & 4 deletions lib/msf/core/trace/certificate_trace_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,16 @@ def initialize(cert = nil)

# Returns a formatted metadata string: subject, issuer, validity, SHA-256 fingerprint.
#
# @param label [String] separator header label (e.g. 'x.509' for an issued
# cert, 'Peer Cert' for a server TLS cert, 'Chain 1/2' for a chain member)
# @return [String, nil] nil if certificate could not be parsed
def to_s_metadata
def to_s_metadata(label: 'x.509')
return nil unless @cert

fingerprint = OpenSSL::Digest::SHA256.hexdigest(@cert.to_der)

[
trace_separator('x.509'),
trace_separator(label),
" Subject : #{@cert.subject}",
" Issuer : #{@cert.issuer}",
" Not Before : #{@cert.not_before}",
Expand All @@ -139,9 +141,10 @@ def to_s_metadata
# Returns a formatted full string: metadata + serial, version, public key algorithm,
# SAN / EKU / Key Usage as named fields, then remaining extensions.
#
# @param label [String] separator header label (see {#to_s_metadata})
# @return [String, nil] nil if certificate could not be parsed
def to_s_full
base = to_s_metadata
def to_s_full(label: 'x.509')
base = to_s_metadata(label: label)
return nil unless base

lines = [base]
Expand Down
Loading
Loading