diff --git a/lib/msf/core/exploit/remote/certificate_trace.rb b/lib/msf/core/exploit/remote/certificate_trace.rb index 457d69daa58a6..5e08fd367c72e 100644 --- a/lib/msf/core/exploit/remote/certificate_trace.rb +++ b/lib/msf/core/exploit/remote/certificate_trace.rb @@ -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 diff --git a/lib/msf/core/exploit/remote/http_client.rb b/lib/msf/core/exploit/remote/http_client.rb index 6a3ab6c79f064..506459c2b82d0 100644 --- a/lib/msf/core/exploit/remote/http_client.rb +++ b/lib/msf/core/exploit/remote/http_client.rb @@ -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 @@ -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 diff --git a/lib/msf/core/exploit/remote/ldap.rb b/lib/msf/core/exploit/remote/ldap.rb index 904c806571dbb..aaba85ddf894e 100644 --- a/lib/msf/core/exploit/remote/ldap.rb +++ b/lib/msf/core/exploit/remote/ldap.rb @@ -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 @@ -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 @@ -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 diff --git a/lib/msf/core/exploit/remote/postgres.rb b/lib/msf/core/exploit/remote/postgres.rb index ec36c9ed1a65f..b6405eda749a9 100644 --- a/lib/msf/core/exploit/remote/postgres.rb +++ b/lib/msf/core/exploit/remote/postgres.rb @@ -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] @@ -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) @@ -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" @@ -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 diff --git a/lib/msf/core/exploit/remote/rdp.rb b/lib/msf/core/exploit/remote/rdp.rb index 966614b822594..7c15fcc067c1d 100644 --- a/lib/msf/core/exploit/remote/rdp.rb +++ b/lib/msf/core/exploit/remote/rdp.rb @@ -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. @@ -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 diff --git a/lib/msf/core/trace/certificate_trace_presenter.rb b/lib/msf/core/trace/certificate_trace_presenter.rb index 4d3d3292afbe8..57be7e92b5886 100644 --- a/lib/msf/core/trace/certificate_trace_presenter.rb +++ b/lib/msf/core/trace/certificate_trace_presenter.rb @@ -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}", @@ -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] diff --git a/spec/lib/msf/core/exploit/remote/certificate_trace_spec.rb b/spec/lib/msf/core/exploit/remote/certificate_trace_spec.rb index 3bc5c5875783b..91451be25ae80 100644 --- a/spec/lib/msf/core/exploit/remote/certificate_trace_spec.rb +++ b/spec/lib/msf/core/exploit/remote/certificate_trace_spec.rb @@ -126,6 +126,11 @@ expect(subject).to receive(:print_line).with(include('Version')) subject.certificate_trace(cert) end + + it 'labels the header x.509 for an issued certificate' do + expect(subject).to receive(:print_line).with(include('[CertificateTrace] x.509')) + subject.certificate_trace(cert) + end end context 'with a PKCS12 bundle' do @@ -182,6 +187,234 @@ end end + describe '#certificate_peer_cert_trace' do + let(:peer_key) { OpenSSL::PKey::RSA.generate(2048) } + + let(:peer_cert) do + c = OpenSSL::X509::Certificate.new + c.subject = OpenSSL::X509::Name.parse('/CN=example.com/O=MSF Lab/C=US') + c.issuer = OpenSSL::X509::Name.parse('/CN=Test CA') + c.public_key = peer_key.public_key + c.serial = OpenSSL::BN.new('42') + c.version = 2 + c.not_before = Time.utc(2026, 1, 1) + c.not_after = Time.utc(2027, 1, 1) + c.sign(peer_key, OpenSSL::Digest::SHA256.new) + c + end + + let(:inactive_db) { double('db', active: false) } + + before do + allow(framework).to receive(:db).and_return(inactive_db) + end + + context 'when tracing is off' do + it 'produces no output' do + subject.datastore['CertificateTrace'] = 'off' + expect(subject).not_to receive(:print_line) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + end + + context 'when cert is nil' do + it 'produces no output' do + subject.datastore['CertificateTrace'] = 'metadata' + expect(subject).not_to receive(:print_line) + subject.certificate_peer_cert_trace(nil, '192.168.1.1', 443) + end + end + + context 'when mode is metadata' do + before { subject.datastore['CertificateTrace'] = 'metadata' } + + it 'prints a line containing [CertificateTrace]' do + expect(subject).to receive(:print_line).with(include('[CertificateTrace]')) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'includes the subject CN' do + expect(subject).to receive(:print_line).with(include('example.com')) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'does not include Serial (metadata omits extended fields)' do + expect(subject).to receive(:print_line).with(satisfy { |s| !s.include?('Serial') }) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + end + + context 'when mode is full' do + before { subject.datastore['CertificateTrace'] = 'full' } + + it 'prints a line containing [CertificateTrace]' do + expect(subject).to receive(:print_line).with(include('[CertificateTrace]')) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'includes Serial in full mode' do + expect(subject).to receive(:print_line).with(include('Serial')) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'includes Version in full mode' do + expect(subject).to receive(:print_line).with(include('Version')) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'labels the header Peer Cert (not x.509) so it is distinct from an issued cert' do + expect(subject).to receive(:print_line).with(satisfy { |s| + s.include?('[CertificateTrace] Peer Cert') && !s.include?('x.509') + }) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + end + + context 'with raw DER bytes' do + before { subject.datastore['CertificateTrace'] = 'metadata' } + + it 'parses and prints without raising' do + expect(subject).to receive(:print_line).with(include('[CertificateTrace]')) + subject.certificate_peer_cert_trace(peer_cert.to_der, '192.168.1.1', 443) + end + end + + context 'with an invalid certificate' do + it 'produces no output and does not raise' do + subject.datastore['CertificateTrace'] = 'full' + expect(subject).not_to receive(:print_line) + expect { subject.certificate_peer_cert_trace('not a cert', '192.168.1.1', 443) }.not_to raise_error + end + end + + context 'in-memory deduplication (no DB)' do + before { subject.datastore['CertificateTrace'] = 'metadata' } + + it 'prints only once for repeated calls to the same host:port' do + expect(subject).to receive(:print_line).once + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'prints again for a different host' do + expect(subject).to receive(:print_line).twice + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.2', 443) + end + + it 'treats the same host on a different port as a separate entry' do + expect(subject).to receive(:print_line).twice + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 8443) + end + end + + context 'DB-backed deduplication' do + let(:host_dbl) { double('host', address: '192.168.1.1') } + let(:service_dbl) { double('service', port: 443) } + let(:note_dbl) { double('note', host: host_dbl, service: service_dbl) } + let(:active_db) { double('db', active: true) } + + before do + subject.datastore['CertificateTrace'] = 'metadata' + allow(framework).to receive(:db).and_return(active_db) + allow(active_db).to receive(:report_note) + end + + it 'skips printing when a matching note already exists in the DB' do + allow(active_db).to receive(:notes).with(ntype: 'ssl.peer_cert_seen').and_return([note_dbl]) + expect(subject).not_to receive(:print_line) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'prints and records a note when the host:port has not been seen before' do + allow(active_db).to receive(:notes).with(ntype: 'ssl.peer_cert_seen').and_return([]) + expect(active_db).to receive(:report_note).with(hash_including( + host: '192.168.1.1', port: 443, ntype: 'ssl.peer_cert_seen' + )) + expect(subject).to receive(:print_line).with(include('[CertificateTrace]')) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'uses the in-memory cache so the DB is not queried on repeated calls' do + allow(active_db).to receive(:notes).with(ntype: 'ssl.peer_cert_seen').and_return([]).once + expect(subject).to receive(:print_line).once + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + end + + context 'color wrapping' do + before { subject.datastore['CertificateTrace'] = 'metadata' } + + it 'uses CertificateTraceColors for peer cert output' do + subject.datastore['CertificateTraceColors'] = 'red/grn' + expect(subject).to receive(:print_line).with(satisfy { |s| + s.include?('%bld%grn') && s.include?('%clr') + }) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + + it 'defaults to red/blu when CertificateTraceColors is not customized' do + expect(subject).to receive(:print_line).with(satisfy { |s| + s.include?('%bld%blu') && s.include?('%clr') + }) + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443) + end + end + + context 'cert chain (full mode)' do + let(:ca_key) { OpenSSL::PKey::RSA.generate(2048) } + + let(:intermediate_cert) do + c = OpenSSL::X509::Certificate.new + c.subject = OpenSSL::X509::Name.parse('/CN=Intermediate CA/O=MSF Lab') + c.issuer = OpenSSL::X509::Name.parse('/CN=Root CA') + c.public_key = ca_key.public_key + c.serial = OpenSSL::BN.new('10') + c.version = 2 + c.not_before = Time.utc(2025, 1, 1) + c.not_after = Time.utc(2030, 1, 1) + c.sign(ca_key, OpenSSL::Digest::SHA256.new) + c + end + + before { subject.datastore['CertificateTrace'] = 'full' } + + it 'prints chain certs when chain has more than one cert' do + chain = [peer_cert, intermediate_cert] + # leaf + 1 chain cert = 2 print_line calls + expect(subject).to receive(:print_line).twice + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443, chain: chain) + end + + it 'uses "Chain N/M" header for intermediate certs' do + chain = [peer_cert, intermediate_cert] + received = [] + allow(subject).to receive(:print_line) { |s| received << s } + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443, chain: chain) + expect(received.last).to include('Chain 1/1') + end + + it 'does not print chain certs in metadata mode' do + subject.datastore['CertificateTrace'] = 'metadata' + chain = [peer_cert, intermediate_cert] + expect(subject).to receive(:print_line).once + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443, chain: chain) + end + + it 'prints only the leaf when chain is nil' do + expect(subject).to receive(:print_line).once + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443, chain: nil) + end + + it 'prints only the leaf when chain has only one cert (the leaf itself)' do + expect(subject).to receive(:print_line).once + subject.certificate_peer_cert_trace(peer_cert, '192.168.1.1', 443, chain: [peer_cert]) + end + end + end + describe '#certificate_csr_trace' do let(:csr) do req = OpenSSL::X509::Request.new diff --git a/spec/lib/msf/core/exploit/remote/ldap_spec.rb b/spec/lib/msf/core/exploit/remote/ldap_spec.rb index ceb98641cf1a2..1ef1a8a967f00 100644 --- a/spec/lib/msf/core/exploit/remote/ldap_spec.rb +++ b/spec/lib/msf/core/exploit/remote/ldap_spec.rb @@ -57,12 +57,140 @@ end describe '#ldap_open' do - it do + let(:ldap_dbl) do + dbl = instance_double(Rex::Proto::LDAP::Client) + allow(dbl).to receive(:instance_variable_get).with(:@open_connection).and_return(nil) + dbl + end + + it 'delegates to Rex::Proto::LDAP::Client.open and forwards the caller block' do opts = {} - expect(Net::LDAP).to receive(:open).with(opts, &ldap_proc) - expect(subject).not_to receive(:get_connect_opts) - expect(subject).to receive(:resolve_connect_opts).and_return(opts) - subject.ldap_open(opts, &ldap_proc) + yielded = nil + allow(subject).to receive(:resolve_connect_opts).and_return(opts) + expect(Rex::Proto::LDAP::Client).to receive(:open).with(opts).and_yield(ldap_dbl) + subject.ldap_open(opts) { |ldap| yielded = ldap } + expect(yielded).to eq(ldap_dbl) + end + + context 'when keep_open is true' do + it 'calls _open and returns the client directly without yielding' do + opts = {} + allow(subject).to receive(:resolve_connect_opts).and_return(opts) + allow(ldap_dbl).to receive(:socket).and_return(nil) + expect(Rex::Proto::LDAP::Client).to receive(:_open).with(opts).and_return(ldap_dbl) + result = subject.ldap_open(opts, keep_open: true) + expect(result).to eq(ldap_dbl) + end + end + + context 'peer cert tracing (keep_open: false)' do + let(:tls_socket) do + dbl = double('socket') + allow(dbl).to receive(:respond_to?).with(:peer_cert).and_return(true) + allow(dbl).to receive(:respond_to?).with(:peer_cert_chain).and_return(true) + allow(dbl).to receive(:peer_cert).and_return(tls_cert) + allow(dbl).to receive(:peer_cert_chain).and_return([tls_cert]) + dbl + end + + let(:tls_conn) do + dbl = double('connection') + allow(dbl).to receive(:socket).and_return(tls_socket) + dbl + end + + let(:tls_ldap_dbl) do + dbl = instance_double(Rex::Proto::LDAP::Client) + allow(dbl).to receive(:instance_variable_get).with(:@open_connection).and_return(tls_conn) + dbl + end + + let(:tls_cert) do + key = OpenSSL::PKey::RSA.generate(2048) + c = OpenSSL::X509::Certificate.new + c.subject = OpenSSL::X509::Name.parse('/CN=ldap.example.com') + c.issuer = OpenSSL::X509::Name.parse('/CN=Test CA') + c.public_key = key.public_key + c.serial = OpenSSL::BN.new('1') + c.version = 2 + c.not_before = Time.utc(2026, 1, 1) + c.not_after = Time.utc(2027, 1, 1) + c.sign(key, OpenSSL::Digest::SHA256.new) + c + end + + before do + subject.datastore['CertificateTrace'] = 'metadata' + allow(subject).to receive(:framework).and_return(framework) + allow(framework).to receive(:db).and_return(double('db', active: false)) + allow(Rex::Proto::LDAP::Client).to receive(:open).and_yield(tls_ldap_dbl) + end + + it 'prints the peer cert when CertificateTrace is enabled and connection is TLS' do + expect(subject).to receive(:print_line).with(include('[CertificateTrace]')) + subject.ldap_open({}) { |_ldap| } + end + + it 'produces no output when CertificateTrace is off' do + subject.datastore['CertificateTrace'] = 'off' + expect(subject).not_to receive(:print_line) + subject.ldap_open({}) { |_ldap| } + end + end + + context 'peer cert tracing (keep_open: true)' do + let(:tls_socket) do + dbl = double('socket') + allow(dbl).to receive(:respond_to?).with(:peer_cert).and_return(true) + allow(dbl).to receive(:respond_to?).with(:peer_cert_chain).and_return(true) + allow(dbl).to receive(:peer_cert).and_return(tls_cert) + allow(dbl).to receive(:peer_cert_chain).and_return([tls_cert]) + dbl + end + + let(:keep_open_ldap_dbl) do + dbl = instance_double(Rex::Proto::LDAP::Client) + allow(dbl).to receive(:socket).and_return(tls_socket) + dbl + end + + let(:tls_cert) do + key = OpenSSL::PKey::RSA.generate(2048) + c = OpenSSL::X509::Certificate.new + c.subject = OpenSSL::X509::Name.parse('/CN=ldap.example.com') + c.issuer = OpenSSL::X509::Name.parse('/CN=Test CA') + c.public_key = key.public_key + c.serial = OpenSSL::BN.new('1') + c.version = 2 + c.not_before = Time.utc(2026, 1, 1) + c.not_after = Time.utc(2027, 1, 1) + c.sign(key, OpenSSL::Digest::SHA256.new) + c + end + + before do + subject.datastore['CertificateTrace'] = 'metadata' + allow(subject).to receive(:framework).and_return(framework) + allow(framework).to receive(:db).and_return(double('db', active: false)) + allow(Rex::Proto::LDAP::Client).to receive(:_open).and_return(keep_open_ldap_dbl) + end + + it 'prints the peer cert when CertificateTrace is enabled and connection is TLS' do + expect(subject).to receive(:print_line).with(include('[CertificateTrace]')) + subject.ldap_open({}, keep_open: true) + end + + it 'produces no output when CertificateTrace is off' do + subject.datastore['CertificateTrace'] = 'off' + expect(subject).not_to receive(:print_line) + subject.ldap_open({}, keep_open: true) + end + + it 'produces no output when the socket has no peer_cert (plain LDAP)' do + allow(tls_socket).to receive(:respond_to?).with(:peer_cert).and_return(false) + expect(subject).not_to receive(:print_line) + subject.ldap_open({}, keep_open: true) + end end end diff --git a/spec/lib/msf/core/exploit/remote/postgres_spec.rb b/spec/lib/msf/core/exploit/remote/postgres_spec.rb new file mode 100644 index 0000000000000..26fd85877a8d3 --- /dev/null +++ b/spec/lib/msf/core/exploit/remote/postgres_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Msf::Exploit::Remote::Postgres do + include_context 'Msf::Simple::Framework' + + subject do + mod = ::Msf::Exploit.new + mod.extend described_class + mod + end + + before do + allow(subject).to receive(:framework).and_return(framework) + subject.datastore['RHOST'] = '192.168.1.20' + subject.datastore['RPORT'] = 5432 + subject.datastore['DATABASE'] = 'template1' + subject.datastore['USERNAME'] = 'postgres' + subject.datastore['PASSWORD'] = 'postgres' + end + + describe '#postgres_login' do + let(:tls_cert) do + key = OpenSSL::PKey::RSA.generate(2048) + c = OpenSSL::X509::Certificate.new + c.subject = OpenSSL::X509::Name.parse('/CN=pg.example.com') + c.issuer = OpenSSL::X509::Name.parse('/CN=Test CA') + c.public_key = key.public_key + c.serial = OpenSSL::BN.new('1') + c.version = 2 + c.not_before = Time.utc(2026, 1, 1) + c.not_after = Time.utc(2027, 1, 1) + c.sign(key, OpenSSL::Digest::SHA256.new) + c + end + + let(:ssl_socket_dbl) do + dbl = double('ssl_socket') + allow(dbl).to receive(:peer_cert).and_return(tls_cert) + dbl + end + + let(:pg_conn_dbl) do + dbl = double('pg_connection') + allow(dbl).to receive(:peerhost).and_return('192.168.1.20') + allow(dbl).to receive(:peerport).and_return(5432) + allow(dbl).to receive(:conn).and_return(ssl_socket_dbl) + dbl + end + + before do + allow(::Msf::Db::PostgresPR::Connection).to receive(:new).and_return(pg_conn_dbl) + allow(framework).to receive(:db).and_return(double('db', active: false)) + end + + context 'SSL peer cert tracing' do + before { subject.datastore['SSL'] = true } + + it 'prints the peer cert when CertificateTrace is enabled' do + subject.datastore['CertificateTrace'] = 'metadata' + expect(subject).to receive(:print_line).with(include('[CertificateTrace]')) + subject.postgres_login + end + + it 'produces no output when CertificateTrace is off' do + subject.datastore['CertificateTrace'] = 'off' + expect(subject).not_to receive(:print_line) + expect(subject.postgres_login).to eq(:connected) + end + + it 'passes ssl: true through to Connection.new' do + expect(::Msf::Db::PostgresPR::Connection).to receive(:new).with( + anything, anything, anything, anything, anything, true + ).and_return(pg_conn_dbl) + subject.postgres_login + end + end + + context 'plain connection (SSL disabled)' do + before { subject.datastore['SSL'] = false } + + it 'produces no output even when CertificateTrace is enabled (no peer_cert)' do + allow(ssl_socket_dbl).to receive(:peer_cert).and_return(nil) + subject.datastore['CertificateTrace'] = 'metadata' + expect(subject).not_to receive(:print_line) + expect(subject.postgres_login).to eq(:connected) + end + + it 'passes ssl: false through to Connection.new' do + expect(::Msf::Db::PostgresPR::Connection).to receive(:new).with( + anything, anything, anything, anything, anything, false + ).and_return(pg_conn_dbl) + subject.postgres_login + end + end + + context 'opts[:ssl] override' do + it 'respects an explicit ssl: true passed in opts even when datastore SSL is false' do + subject.datastore['SSL'] = false + expect(::Msf::Db::PostgresPR::Connection).to receive(:new).with( + anything, anything, anything, anything, anything, true + ).and_return(pg_conn_dbl) + subject.postgres_login(ssl: true) + end + end + + context 'when login fails' do + it 'returns :error_credentials on bad password without calling certificate_peer_cert_trace' do + allow(::Msf::Db::PostgresPR::Connection).to receive(:new).and_raise(RuntimeError, "auth\tC28000") + expect(subject).not_to receive(:certificate_peer_cert_trace) + expect(subject.postgres_login).to eq(:error_credentials) + end + end + end +end diff --git a/spec/lib/msf/core/exploit/remote/rdp_spec.rb b/spec/lib/msf/core/exploit/remote/rdp_spec.rb new file mode 100644 index 0000000000000..9e64916842a8e --- /dev/null +++ b/spec/lib/msf/core/exploit/remote/rdp_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Msf::Exploit::Remote::RDP do + include_context 'Msf::Simple::Framework' + + subject do + mod = ::Msf::Exploit.new + mod.extend described_class + mod + end + + before do + allow(subject).to receive(:framework).and_return(framework) + allow(subject).to receive(:rhost).and_return('192.168.1.10') + allow(subject).to receive(:rport).and_return(3389) + end + + describe '#swap_sock_plain_to_ssl' do + let(:tls_cert) do + key = OpenSSL::PKey::RSA.generate(2048) + c = OpenSSL::X509::Certificate.new + c.subject = OpenSSL::X509::Name.parse('/CN=rdp.example.com') + c.issuer = OpenSSL::X509::Name.parse('/CN=Test CA') + c.public_key = key.public_key + c.serial = OpenSSL::BN.new('1') + c.version = 2 + c.not_before = Time.utc(2026, 1, 1) + c.not_after = Time.utc(2027, 1, 1) + c.sign(key, OpenSSL::Digest::SHA256.new) + c + end + + let(:ssl_socket_dbl) do + dbl = double('ssl_socket') + allow(dbl).to receive(:connect) + allow(dbl).to receive(:peer_cert).and_return(tls_cert) + allow(dbl).to receive(:peer_cert_chain).and_return([tls_cert]) + dbl + end + + let(:rdp_sock_dbl) do + dbl = double('rdp_sock') + allow(dbl).to receive(:extend) + allow(dbl).to receive(:sslsock=) + allow(dbl).to receive(:sslctx=) + dbl + end + + before do + subject.instance_variable_set(:@rdp_sock, rdp_sock_dbl) + subject.datastore['RDP_TLS_SECURITY_LEVEL'] = 0 + allow(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket_dbl) + allow(framework).to receive(:db).and_return(double('db', active: false)) + end + + it 'prints the peer cert when CertificateTrace is enabled' do + subject.datastore['CertificateTrace'] = 'metadata' + expect(subject).to receive(:print_line).with(include('[CertificateTrace]')) + subject.swap_sock_plain_to_ssl + end + + it 'produces no output when CertificateTrace is off' do + subject.datastore['CertificateTrace'] = 'off' + expect(subject).not_to receive(:print_line) + subject.swap_sock_plain_to_ssl + end + + it 'produces no output when the server returns no cert' do + subject.datastore['CertificateTrace'] = 'metadata' + allow(ssl_socket_dbl).to receive(:peer_cert).and_return(nil) + expect(subject).not_to receive(:print_line) + subject.swap_sock_plain_to_ssl + end + end +end