Skip to content

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

Open
Pushpenderrathore wants to merge 15 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 15 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

Rebased onto latest master; the conflicts with the merged CSR trace work (#21580) were resolved.

Chain header rendering: chain members are printed by passing label: "Chain N/M" to CertificateTracePresenter#to_s_full. This relies on the same label: keyword described below (default x.509), so there is no dependency on presenter separator internals and the header pads to the presenter's separator width for any chain length. (An earlier revision referenced CertificateTracePresenter::SEPARATOR, a constant #21580 removed; that path is gone.)

Peer cert labeling: peer certs now print under a Peer Cert header (a label: keyword was added to CertificateTracePresenter#to_s_metadata/#to_s_full, default x.509), so a server's TLS certificate is no longer visually indistinguishable from an issued/client certificate. Issued-cert (x.509) and CSR output are unchanged.

Unit specs (this branch, post-merge):

  • bundle exec rspec spec/lib/msf/core/exploit/remote/certificate_trace_spec.rb - 59 examples, 0 failures (includes 5 cert chain (full mode) examples)
  • bundle exec rspec spec/lib/msf/core/exploit/remote/ldap_spec.rb - 31 examples, 0 failures
  • bundle exec rspec spec/lib/msf/core/exploit/remote/rdp_spec.rb - 10 examples, 0 failures
  • bundle exec rspec spec/lib/msf/core/exploit/remote/postgres_spec.rb - 14 examples, 0 failures

Live test - Postgres peer cert trace over TLS (auxiliary/admin/postgres/postgres_sql vs a local PostgreSQL 14 with ssl=on, SSL true, CertificateTrace=full). The SQL query succeeds and the cert traces:

[CertificateTrace] Peer Cert ----------------------------
  Subject    : /CN=localhost/O=MSF Lab/C=US
  Issuer     : /CN=MSF Lab Root CA/O=MSF Lab/C=US
  SHA-256    : b3c6c44dd77622d1c73bddcfb2ba1cae5b7655371daf5909dfdd44fcb8bf88b1
  Public Key : RSA-2048
  Identity   : localhost (Subject CN)
  SAN        : DNS:localhost, IP Address:127.0.0.1
  EKU        : TLS Web Server Authentication
  Key Usage  : Digital Signature, Key Encipherment

Live test - full cert chain rendering. Postgres was configured to present a CA-signed server cert (leaf + issuing CA). In full mode the intermediate is printed under a Chain N/M header (leaf under Peer Cert), confirming the chain rendering at runtime:

[CertificateTrace] Chain 1/1 ----------------------------
  Subject    : /CN=MSF Lab Root CA/O=MSF Lab/C=US
  Issuer     : /CN=MSF Lab Root CA/O=MSF Lab/C=US
  Not Before : 2026-07-09 14:04:51 UTC
  Not After  : 2036-07-06 14:04:51 UTC
  SHA-256    : 8edf93a8b0535b8529e8a691c26757aa82e8f9b905970c71d0423899923770c0
  Serial     : 70724844633869769291751631124338764489961695300
  Version    : v3
  Public Key : RSA-2048
  Identity   : MSF Lab Root CA (Subject CN)
  Extensions :
    subjectKeyIdentifier : B7:08:CF:B7:D0:8D:5B:DB:15:2F:0E:6E:80:46:67:1D:6A:09:BC:73
    authorityKeyIdentifier : B7:08:CF:B7:D0:8D:5B:DB:15:2F:0E:6E:80:46:67:1D:6A:09:BC:73
    basicConstraints : CA:TRUE
  • Dedup confirmed: one trace per host:port (DB-backed note when a database is connected, in-memory otherwise).
  • Plain connections (SSL false, no peer cert) produce no 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
@smcintyre-r7 smcintyre-r7 added the GSoC Google Summer of Code project PRs label Jul 8, 2026
Resolve conflicts from the merged CSR trace work (rapid7#21580):
- certificate_trace.rb: keep both certificate_peer_cert_trace (with
  chain support) and certificate_csr_trace as sibling methods
- certificate_trace_spec.rb: keep both describe blocks as siblings

Fix stale constant: the chain-printing path referenced
CertificateTracePresenter::SEPARATOR, which rapid7#21580 removed in favor of
the trace_separator(label) helper. Replace the leading x.509 separator
line of each chain cert's full output via a first-line substitution so
the Chain N/M header still renders. ldap/rdp/postgres and their specs
merged cleanly.
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

Merged latest master and resolved the conflicts. One fix worth flagging for review:

The chain-printing path in certificate_peer_cert_trace referenced Msf::Trace::CertificateTracePresenter::SEPARATOR, which the CSR trace work (#21580) removed from master in favor of the private trace_separator(label) helper. Left as-is, the first full-mode chain print would have raised NameError: uninitialized constant SEPARATOR.

Fix: instead of substituting on the deleted constant, I replace the leading x.509 separator line that to_s_full now emits, via a first-line substitution:

chain_output = chain_output.sub(/\A.*\n/, "#{chain_header}\n")

This keeps the Chain N/M header rendering without depending on a private method or the removed constant.

Verification: full spec suite passes (81 examples, 0 failures), including the cert chain (full mode) context (5 examples) that exercises this path. The presenter took master's trace_separator version cleanly; ldap/rdp/postgres and their specs merged without conflict. Branch is now mergeable.

Addresses review feedback that a server's TLS peer certificate printed
under the same '[CertificateTrace] x.509' header as an issued/client
certificate, making the two indistinguishable.

Add an optional label: keyword to CertificateTracePresenter#to_s_metadata
and #to_s_full (default 'x.509', backward compatible). certificate_peer_cert_trace
now renders peer certs under a 'Peer Cert' header. Issued-cert and CSR
output are unchanged.
With to_s_full now accepting a label: keyword, the chain-printing path
passes label: "Chain N/M" directly to the presenter instead of building
a header string and substituting it into to_s_full's output. This removes
the manual first-line sub, drops the hardcoded dash width (headers now pad
to the presenter's separator width for any chain length), and no longer
depends on any presenter separator internals.
@Pushpenderrathore

Pushpenderrathore commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the peer-cert labeling feedback, and used the same mechanism to simplify the chain rendering.

Added a label: keyword to CertificateTracePresenter#to_s_metadata/#to_s_full (default x.509). Peer certs now render under a Peer Cert header, and chain members render via to_s_full(label: "Chain N/M"). This replaces the earlier first-line substitution (which had referenced the now-removed SEPARATOR constant): no more string surgery, no dependency on presenter separator internals, and the header pads to the correct width for any chain length.

Verified live in full mode against a CA-signed Postgres cert (leaf + issuing CA). The leaf prints under Peer Cert, the CA under Chain 1/1:

[CertificateTrace] Peer Cert ----------------------------
  Subject    : /CN=localhost/O=MSF Lab/C=US
  Issuer     : /CN=MSF Lab Root CA/O=MSF Lab/C=US
  Not Before : 2026-07-09 14:04:51 UTC
  Not After  : 2028-10-11 14:04:51 UTC
  SHA-256    : b3c6c44dd77622d1c73bddcfb2ba1cae5b7655371daf5909dfdd44fcb8bf88b1
  Serial     : 701865896519849359655412527632047902722794025696
  Version    : v3
  Public Key : RSA-2048
  Identity   : localhost (Subject CN)
  SAN        : DNS:localhost, IP Address:127.0.0.1
  EKU        : TLS Web Server Authentication
  Key Usage  : Digital Signature, Key Encipherment
  Extensions :
    subjectKeyIdentifier : EC:C5:D1:0F:28:E2:8A:38:30:AB:63:FE:D9:74:1F:92:BB:36:9E:7F
    authorityKeyIdentifier : B7:08:CF:B7:D0:8D:5B:DB:15:2F:0E:6E:80:46:67:1D:6A:09:BC:73
[CertificateTrace] Chain 1/1 ----------------------------
  Subject    : /CN=MSF Lab Root CA/O=MSF Lab/C=US
  Issuer     : /CN=MSF Lab Root CA/O=MSF Lab/C=US
  Not Before : 2026-07-09 14:04:51 UTC
  Not After  : 2036-07-06 14:04:51 UTC
  SHA-256    : 8edf93a8b0535b8529e8a691c26757aa82e8f9b905970c71d0423899923770c0
  Serial     : 70724844633869769291751631124338764489961695300
  Version    : v3
  Public Key : RSA-2048
  Identity   : MSF Lab Root CA (Subject CN)
  Extensions :
    subjectKeyIdentifier : B7:08:CF:B7:D0:8D:5B:DB:15:2F:0E:6E:80:46:67:1D:6A:09:BC:73
    authorityKeyIdentifier : B7:08:CF:B7:D0:8D:5B:DB:15:2F:0E:6E:80:46:67:1D:6A:09:BC:73
    basicConstraints : CA:TRUE

All specs green (certificate_trace 59, ldap 31, rdp 10, postgres 14).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

additional-testing-required GSoC Google Summer of Code project PRs

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants