Skip to content

Add Postgres TLS support and peer cert tracing via CertificateTrace (Phase 3)#21608

Open
Pushpenderrathore wants to merge 12 commits into
rapid7:masterfrom
Pushpenderrathore:feature/ssl-peer-cert-trace-phase3
Open

Add Postgres TLS support and peer cert tracing via CertificateTrace (Phase 3)#21608
Pushpenderrathore wants to merge 12 commits into
rapid7:masterfrom
Pushpenderrathore:feature/ssl-peer-cert-trace-phase3

Conversation

@Pushpenderrathore

@Pushpenderrathore Pushpenderrathore commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends CertificateTrace peer cert tracing to PostgreSQL over TLS, and fixes a long-standing gap where the Postgres mixin accepted an SSL datastore option (registered via the included Exploit::Remote::Tcp) but never passed it through to the underlying connection.

Changes:

  • Exploit::Remote::Postgres now includes CertificateTrace
  • postgres_login extracts ssl from opts[:ssl] or datastore['SSL'] and passes it as the 6th argument to Connection.new, activating the starttls path in postgres-pr when SSL is requested
  • After a successful login, certificate_peer_cert_trace is called with postgres_conn.conn.peer_cert -- guarded by respond_to?(:peer_cert) so plain TCP connections are a silent no-op
  • Callers can override with opts[:ssl]: true/false independent of the datastore

No new options introduced. The SSL bool was already registered by Exploit::Remote::Tcp; this PR just wires it through.

Test plan

  • bundle exec rspec spec/lib/msf/core/exploit/remote/postgres_spec.rb -- 7 examples, 0 failures
  • Run auxiliary/admin/postgres/postgres_sql against a PostgreSQL server with SSL enabled, set SSL true and CertificateTrace metadata -- confirm the cert prints once (note: auxiliary/scanner/postgres/postgres_login uses LoginScanner::Postgres internally and bypasses postgres_login; modules that call postgres_login directly are the correct test surface)
  • Confirm plain connections (SSL false) produce no cert output

Notes

Introduces Msf::Exploit::Remote::SslPeerCertTrace with an SSLPeerCertTrace
enum datastore option (off/metadata/full). HttpClient includes the mixin and
calls ssl_peer_cert_trace after each send_recv; the helper reads conn.peer_cert,
deduplicates per host:port, and formats the server certificate through the
existing CertificateTracePresenter.
Removes the standalone SSLPeerCertTrace mixin and SSLPeerCertTrace datastore
option. Peer SSL certificate tracing now lives in CertificateTrace as
#certificate_peer_cert_trace, reusing the existing CertificateTrace enum and
CertificateTraceColors option so all cert output (issued, CSR, peer) is
controlled by a single operator-facing option.

Deduplication now uses the database when active (ssl.peer_cert_seen note per
host:port, update: :unique) so the same server certificate is not reprinted
across module runs. Falls back to in-memory dedup when no DB is connected.

HttpClient is updated to include CertificateTrace instead of SslPeerCertTrace
and calls certificate_peer_cert_trace after send_recv. The ssl_peer_cert_trace
mixin file and its spec are removed; the 20 new peer cert examples are added
to certificate_trace_spec.rb (46 examples, 0 failures).
Includes CertificateTrace in Exploit::Remote::LDAP so LDAP over TLS
connections surface the server certificate through the same CertificateTrace
option and CertificateTraceColors that HTTP uses. No new operator-facing
option is introduced.

ldap_open is updated for both paths:
- keep_open: false (Rex::Proto::LDAP::Client.open) -- wraps the caller
  block and reads peer_cert from the @open_connection socket before
  yielding to the caller.
- keep_open: true (Rex::Proto::LDAP::Client._open) -- reads peer_cert
  from ldap.socket after the connection is established and returned.

A private _ldap_trace_peer_cert helper centralises the socket guard
and the certificate_peer_cert_trace call so both paths share the same
nil-safe check. Plain LDAP connections (no TLS, no peer_cert) are
silently ignored. Dedup and DB recording are handled by
certificate_peer_cert_trace in the CertificateTrace mixin.

Spec: 14 examples (7 new for peer cert tracing), 0 failures.
Includes CertificateTrace in Exploit::Remote::RDP so the server TLS
certificate is surfaced when an operator sets CertificateTrace to
metadata or full. The hook is in swap_sock_plain_to_ssl, immediately
after ssl.connect succeeds -- the OpenSSL socket has a valid peer_cert
at that point and before it is wrapped into the Rex SslTcp layer.

No new operator-facing option is introduced; RDP re-uses the same
CertificateTrace enum as HTTP and LDAP. A nil peer_cert (e.g. when
the server sends no certificate) is a silent no-op.

Spec: 3 new examples covering TLS cert printed, off mode silent, and
nil peer_cert silent.
Includes CertificateTrace in Exploit::Remote::Postgres so the server
TLS certificate is surfaced when connecting via SSL (datastore SSL=true).

The SSL option was already registered by the included Exploit::Remote::Tcp
mixin but was never threaded through to Connection.new. This commit passes
it as the sixth positional argument, activating the starttls path in
postgres-pr. After a successful login the leaf certificate is read from
postgres_conn.conn.peer_cert and forwarded to certificate_peer_cert_trace.

A nil peer_cert (plain TCP connection with SSL=false) is a silent no-op.
Callers can override with opts[:ssl] to force TLS regardless of the
datastore, matching the existing pattern for other opts keys in
postgres_login.

Spec: 7 new examples covering TLS cert printed, off mode silent, ssl arg
threading, plain connection no-op, opts[:ssl] override, and failed login
no-op.
@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.

When CertificateTrace=full and the server sends a chain during the TLS
handshake, the intermediate and root CA certs are now printed after the
leaf cert. Each chain cert uses a "Chain N/M" position header in place
of the standard [CertificateTrace] separator so operators can visually
distinguish chain depth.

certificate_peer_cert_trace gains an optional chain: keyword argument
(array of OpenSSL::X509::Certificate). chain[0] is the peer cert already
printed; chain[1..] are the chain certs. In metadata mode the chain
parameter is silently ignored so verbosity stays minimal.

Protocol hooks updated to pass peer_cert_chain:
- HTTP (HttpClient):  c.conn.peer_cert_chain
- LDAP (_ldap_trace_peer_cert): socket.peer_cert_chain via respond_to? guard
- RDP (swap_sock_plain_to_ssl): ssl.peer_cert_chain (OpenSSL::SSL::SSLSocket)
- Postgres (postgres_login): pg_socket.peer_cert_chain via respond_to? guard

Spec: 5 new chain examples in certificate_trace_spec; LDAP and RDP specs
updated to stub peer_cert_chain on their socket doubles. 75 examples, 0
failures across all Phase 1-4 specs.
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

Lab test: Postgres TLS cert tracing

Tested against a local PostgreSQL 14 instance (SSL enabled, self-signed cert) using auxiliary/admin/postgres/postgres_sql with SSL true.

metadata mode:

msf6 auxiliary(admin/postgres/postgres_sql) > use auxiliary/admin/postgres/postgres_sql
msf6 auxiliary(admin/postgres/postgres_sql) > set RHOSTS 127.0.0.1
msf6 auxiliary(admin/postgres/postgres_sql) > set RPORT 5432
msf6 auxiliary(admin/postgres/postgres_sql) > set USERNAME msf_test
msf6 auxiliary(admin/postgres/postgres_sql) > set PASSWORD msf_test_pass
msf6 auxiliary(admin/postgres/postgres_sql) > set DATABASE msf_testdb
msf6 auxiliary(admin/postgres/postgres_sql) > set SSL true
msf6 auxiliary(admin/postgres/postgres_sql) > set CertificateTrace metadata
msf6 auxiliary(admin/postgres/postgres_sql) > run

[*] Running module against 127.0.0.1
[CertificateTrace] --------------------------------------
  Subject    : /CN=localhost/O=MSF Lab/C=US
  Issuer     : /CN=localhost/O=MSF Lab/C=US
  Not Before : 2026-06-25 18:52:51 UTC
  Not After  : 2027-06-25 18:52:51 UTC
  SHA-256    : 47ca038266617954689913a1ba9f40e356e7be4e3cd9363e64a791eae46acd37

Query Text: 'select version()'
==============================
    version
    -------
    PostgreSQL 14.22 (Homebrew) on aarch64-apple-darwin25.2.0, compiled by Apple clang version 17.0.0 (clang-1700.6.3.2), 64-bit

[*] Auxiliary module execution completed

full mode (extended cert fields + chain):

[CertificateTrace] --------------------------------------
  Subject    : /CN=localhost/O=MSF Lab/C=US
  Issuer     : /CN=localhost/O=MSF Lab/C=US
  Not Before : 2026-06-25 18:52:51 UTC
  Not After  : 2027-06-25 18:52:51 UTC
  SHA-256    : 47ca038266617954689913a1ba9f40e356e7be4e3cd9363e64a791eae46acd37
  Serial     : 399776067286582776530792787929148488754714636072
  Version    : v3
  Public Key : RSA-2048
  Identity   : localhost (Subject CN)
  Extensions :
    subjectKeyIdentifier       : 9E:4E:6C:30:04:D7:B0:4A:C4:8A:D5:21:76:7B:BF:71:67:BA:14:99
    authorityKeyIdentifier     : 9E:4E:6C:30:04:D7:B0:4A:C4:8A:D5:21:76:7B:BF:71:67:BA:14:99
    basicConstraints           : CA:TRUE

Notes:

  • postgres_login (the mixin method) is the correct hook point. auxiliary/scanner/postgres/postgres_login uses Metasploit::Framework::LoginScanner::Postgres internally which bypasses this hook -- any module that calls postgres_login directly will get cert tracing (e.g. auxiliary/admin/postgres/postgres_sql, auxiliary/admin/postgres/postgres_readfile, etc.)
  • DB-backed dedup working: cert is recorded once per host:port and skipped on subsequent runs
  • full mode shows extended fields (Serial, Version, Public Key, Extensions)
  • Self-signed cert on PostgreSQL 14 with ssl = on in postgresql.conf

Plain (non-SSL) connections return a raw Socket which does not have
peer_cert, causing a NoMethodError. Match the pattern used in the LDAP
and HTTP hooks.
LoginScanner::LDAP includes Exploit::Remote::LDAP so it calls
_ldap_trace_peer_cert via ldap_open. LoginScanner does not have
datastore, rhost, or rport. Guard with certificate_trace_enabled?
early so we return before evaluating rhost/rport arguments when
tracing is not available (e.g. in scanner context).
LoginScanner::LDAP includes Exploit::Remote::LDAP so it calls
_ldap_trace_peer_cert via ldap_open. LoginScanner does not have
datastore, so calling rhost (which calls datastore['RHOST']) raises
NoMethodError, caught as UNABLE_TO_CONNECT in the scanner.

Guard with certificate_trace_enabled? at the top so we return before
evaluating rhost/rport when tracing is unavailable.
certificate_trace already tested the invalid-cert no-raise case;
add the same coverage for certificate_peer_cert_trace.
@Pushpenderrathore Pushpenderrathore marked this pull request as ready for review June 26, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants