Skip to content

Add LDAP and RDP TLS peer cert tracing via CertificateTrace (Phase 2)#21607

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

Add LDAP and RDP TLS peer cert tracing via CertificateTrace (Phase 2)#21607
Pushpenderrathore wants to merge 7 commits into
rapid7:masterfrom
Pushpenderrathore:feature/ssl-peer-cert-trace-phase2

Conversation

@Pushpenderrathore

@Pushpenderrathore Pushpenderrathore commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends CertificateTrace peer cert tracing (introduced in #21599) to LDAP over TLS and RDP. No new operator-facing option -- operators use the existing CertificateTrace enum (off/metadata/full).

LDAP (Exploit::Remote::LDAP):

  • Includes CertificateTrace
  • ldap_open wraps both connection paths (keep_open: false via Rex::Proto::LDAP::Client.open; keep_open: true via Client._open) to read the server cert from the socket after the TLS handshake
  • Private _ldap_trace_peer_cert helper guards against non-TLS (plain LDAP) connections silently

RDP (Exploit::Remote::RDP):

  • Includes CertificateTrace
  • swap_sock_plain_to_ssl calls certificate_peer_cert_trace immediately after ssl.connect while the raw OpenSSL::SSL::SSLSocket is still in scope

Dedup, color, and DB recording are delegated to #certificate_peer_cert_trace in CertificateTrace (the same logic as HTTP).

Test plan

  • bundle exec rspec spec/lib/msf/core/exploit/remote/ldap_spec.rb -- 21 examples, 0 failures (7 new peer cert examples)
  • bundle exec rspec spec/lib/msf/core/exploit/remote/rdp_spec.rb -- 3 examples, 0 failures
  • Run an LDAP module against an LDAPS (port 636) target with set CertificateTrace metadata and confirm cert prints once
  • Run an RDP scanner against a Windows target and confirm cert prints once

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.
@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.

@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

Live lab validation against Windows Server 2022 AD CS DC (192.168.64.3)

LDAPS port 636 -- metadata mode

[*] Running module against 192.168.64.3
[CertificateTrace] --------------------------------------
  Subject    : /CN=DC1.kerberos.issue
  Issuer     : /DC=issue/DC=kerberos/CN=kerberos-DC1-CA
  Not Before : 2026-06-04 08:35:58 UTC
  Not After  : 2027-06-04 08:35:58 UTC
  SHA-256    : 5ba07d93b9243d781045c37db3fd3d0573e9dbc2f7be11d8fa5511f603dfba61

LDAPS port 636 -- full mode (shows AD CS template, SAN, EKU, extensions)

[CertificateTrace] --------------------------------------
  Subject    : /CN=DC1.kerberos.issue
  Issuer     : /DC=issue/DC=kerberos/CN=kerberos-DC1-CA
  Not Before : 2026-06-04 08:35:58 UTC
  Not After  : 2027-06-04 08:35:58 UTC
  SHA-256    : 5ba07d93b9243d781045c37db3fd3d0573e9dbc2f7be11d8fa5511f603dfba61
  Serial     : 624420865570961312750736130701723500422365186
  Version    : v3
  Public Key : RSA-2048
  Identity   : DC1.kerberos.issue (Subject CN)
  SAN        : othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC1.kerberos.issue
  EKU        : TLS Web Client Authentication, TLS Web Server Authentication
  Key Usage  : Digital Signature, Key Encipherment
  Extensions :
    Certificate Template Name : DomainController
    subjectKeyIdentifier : 4C:79:01:21:89:99:80:CE:0F:4A:57:08:36:9D:28:C9:A5:41:98:F8
    authorityKeyIdentifier : 66:B8:C4:19:DF:75:62:23:F4:A4:08:92:C3:8F:61:EB:BF:BC:5E:06
    crlDistributionPoints : Full Name: URI:ldap:///CN=kerberos-DC1-CA,...
    authorityInfoAccess : CA Issuers - URI:ldap:///CN=kerberos-DC1-CA,...

RDP port 3389

[CertificateTrace] --------------------------------------
  Subject    : /CN=DC1.kerberos.issue
  Issuer     : /CN=DC1.kerberos.issue
  Not Before : 2026-06-25 06:20:44 UTC
  Not After  : 2026-12-25 06:20:44 UTC
  SHA-256    : e2b701ff59a955e75d8cb06395fdc112de068a00a616ac1e0d562c991a6167ec

RDP produces a self-signed cert (Windows default) -- Subject equals Issuer, 6-month validity. Correctly identified as self-signed with no chain.

Dedup (cross-run, DB-backed)

Second run of the same module against the same host:port with CertificateTrace full produces no cert output -- the ssl.peer_cert_seen DB note from the first run suppresses it. The notes command confirms the note is present.

Notes

The ldap_query module fails with a schema query error after the cert prints -- this is pre-existing and unrelated to the tracing code (anonymous bind has insufficient privileges for schema discovery).

@Pushpenderrathore Pushpenderrathore marked this pull request as ready for review June 25, 2026 17:57
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.
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

CI fix: LoginScanner::LDAP rspec failures

The rspec-rerun:spec --tag ~content jobs were failing with 10 failures in spec/lib/metasploit/framework/login_scanner/ldap_spec.rb. All failures showed "Unable to Connect" instead of "Successful" / "Incorrect".

Root cause: LoginScanner::LDAP includes Exploit::Remote::LDAP, so our new ldap_open hook calls _ldap_trace_peer_cert in scanner context. The scanner's RSpec doubles are spies that respond to any method (including peer_cert), so the respond_to?(:peer_cert) guard passed. The code then evaluated rhost and rport as arguments to certificate_peer_cert_trace -- but rhost internally calls datastore['RHOST'], and LoginScanner::LDAP has no datastore. That NoMethodError was caught by do_login's rescue StandardError and returned as UNABLE_TO_CONNECT.

Fix: Add return unless certificate_trace_enabled? at the top of _ldap_trace_peer_cert. certificate_trace_enabled? already guards on respond_to?(:datastore) && datastore, so it returns false in scanner context before rhost/rport are ever evaluated. Committed in 91f36cef.

certificate_trace already tested the invalid-cert no-raise case;
add the same coverage for certificate_peer_cert_trace.
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