Class: Msf::Trace::CertificateTracePresenter

Inherits:
Object
  • Object
show all
Defined in:
lib/msf/core/trace/certificate_trace_presenter.rb

Overview

Presenter for X.509 certificates.

Follows the same responsibility split as Rex::Proto::Kerberos::CredentialCache::Krb5CCachePresenter: this class constructs formatted strings only. The caller (module instance) is responsible for invoking its own print methods so output is correctly associated with the running module.

Usage:

presenter = Msf::Trace::CertificateTracePresenter.new(cert)
mod.print_line(presenter.)
mod.print_line(presenter.to_s_full)

# CSR mode - present a certificate signing request before it is submitted
# (e.g. the AD CS / MS-ICPR enrollment flow). +attributes+ is the enrollment
# attribute hash returned alongside the CSR by CertRequest#create_csr.
presenter = Msf::Trace::CertificateTracePresenter.new
mod.print_line(presenter.(csr))
mod.print_line(presenter.to_s_csr_full(csr, attributes))

Constant Summary collapse

SEPARATOR_WIDTH =

‘[CertificateTrace] ’ (19) + 38 dashes, preserved

57
NAMED_OIDS =

OIDs surfaced as named fields in to_s_full; excluded from the raw extension dump.

%w[subjectAltName extendedKeyUsage keyUsage].freeze
CERT_TEMPLATE_NAME_OID =

Microsoft AD CS enrollment extensions whose content carries the template name / template version. OpenSSL has no friendly decoder for these, so we decode them ourselves below.

'1.3.6.1.4.1.311.20.2'
CERT_TEMPLATE_INFO_OID =
'1.3.6.1.4.1.311.21.7'
APPLICATION_POLICIES_OID =

Microsoft Application Policies extension. Its content is a CertificatePolicies SEQUENCE OF PolicyInformation, which the framework already decodes for the icpr_cert workflow; we resolve each policy OID to its friendly label rather than dumping the raw bytes (the policy OIDs - e.g. Client Authentication - are central to ESC attack triage).

'1.3.6.1.4.1.311.21.10'
EXTENSION_OID_NAMES =

Friendly labels for extension OIDs that OpenSSL leaves as raw numeric strings (predominantly Microsoft enrollment OIDs on AD CS certificates).

{
  CERT_TEMPLATE_NAME_OID => 'Certificate Template Name',
  CERT_TEMPLATE_INFO_OID => 'Certificate Template Information',
  APPLICATION_POLICIES_OID => 'Application Policies',
  '1.3.6.1.4.1.311.25.2' => 'AD DS Security Extension (SID)'
}.freeze
OPENSSL_READABLE_EXTENSIONS =

Standard PKIX extensions OpenSSL renders as clean, human-readable text. For any other extension - notably the Microsoft AD CS enrollment OIDs - OpenSSL emits a lossy byte dump (raw bytes on OpenSSL, non-printables collapsed to ‘.’ on LibreSSL), so we hex-encode the raw extnValue instead of printing mojibake. Matched against the short name OpenSSL reports for recognised OIDs.

%w[
  basicConstraints
  authorityKeyIdentifier
  subjectKeyIdentifier
  crlDistributionPoints
  authorityInfoAccess
  certificatePolicies
  issuerAltName
  nameConstraints
  nsComment
  nsCertType
].freeze
IDENTITY_SOURCES =

Priority order for resolving a single auth identity from the cert. UPN is the primary AD identity; email and CN are fallbacks.

[
  [:upn, 'UPN'],
  [:email, 'Email SAN'],
  [:cn, 'Subject CN']
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cert = nil) ⇒ CertificateTracePresenter

Returns a new instance of CertificateTracePresenter.

Parameters:

  • cert (OpenSSL::X509::Certificate, OpenSSL::PKCS12, String, nil) (defaults to: nil)


117
118
119
# File 'lib/msf/core/trace/certificate_trace_presenter.rb', line 117

def initialize(cert = nil)
  @cert = self.class.coerce(cert)
end

Class Method Details

.coerce(cert) ⇒ OpenSSL::X509::Certificate?

Attempt to coerce input into an OpenSSL::X509::Certificate. Accepts a live certificate object, an OpenSSL::PKCS12 bundle (extracts the leaf certificate), or raw DER/PEM bytes.

Parameters:

  • cert (OpenSSL::X509::Certificate, OpenSSL::PKCS12, String)

Returns:

  • (OpenSSL::X509::Certificate, nil)


88
89
90
91
92
93
94
95
96
# File 'lib/msf/core/trace/certificate_trace_presenter.rb', line 88

def self.coerce(cert)
  return cert if cert.is_a?(OpenSSL::X509::Certificate)
  return cert.certificate if cert.is_a?(OpenSSL::PKCS12)
  return OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String)

  nil
rescue OpenSSL::X509::CertificateError, OpenSSL::PKCS12::PKCS12Error
  nil
end

.coerce_csr(csr) ⇒ OpenSSL::X509::Request?

Attempt to coerce input into an OpenSSL::X509::Request (CSR). Accepts a live request object (the type CertRequest#create_csr returns is a plain OpenSSL::X509::Request) or raw DER/PEM bytes. Anything else - for example the CMC ContentInfo wrapper produced for on-behalf-of enrollments - is returned as nil so the caller silently skips the trace.

Parameters:

  • csr (OpenSSL::X509::Request, String, #to_der)

Returns:

  • (OpenSSL::X509::Request, nil)


106
107
108
109
110
111
112
113
114
# File 'lib/msf/core/trace/certificate_trace_presenter.rb', line 106

def self.coerce_csr(csr)
  return csr if csr.is_a?(OpenSSL::X509::Request)
  return OpenSSL::X509::Request.new(csr) if csr.is_a?(String)
  return OpenSSL::X509::Request.new(csr.to_der) if csr.respond_to?(:to_der)

  nil
rescue OpenSSL::X509::RequestError, OpenSSL::ASN1::ASN1Error
  nil
end

Instance Method Details

#to_s_csr_full(csr, attributes = {}) ⇒ String?

Returns a formatted full CSR string: metadata plus the requested template and SAN (taken from the enrollment attributes hash) and any extensions carried in the CSR’s PKCS#9 extensionRequest attribute.

Parameters:

  • csr (OpenSSL::X509::Request, String, #to_der)
  • attributes (Hash) (defaults to: {})

    enrollment request attributes from CertRequest#create_csr (e.g. 'CertificateTemplate', 'SAN')

Returns:

  • (String, nil)

    nil if the CSR could not be parsed



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/msf/core/trace/certificate_trace_presenter.rb', line 213

def to_s_csr_full(csr, attributes = {})
  csr = self.class.coerce_csr(csr)
  base = (csr)
  return nil unless base

  attributes ||= {}
  lines = [base]

  template = attributes['CertificateTemplate'] || attributes[:CertificateTemplate]
  lines << "  Req Template : #{template}" if template

  san = attributes['SAN'] || attributes[:SAN]
  lines << "  Req SAN      : #{san}" if san

  # subjectAltName is already surfaced above as the friendly "Req SAN" line;
  # drop it from the raw dump so we don't repeat it as an opaque hex blob.
  extensions = csr_extensions(csr).reject { |e| e.oid == 'subjectAltName' }
  if extensions.any?
    lines << '  Req Extns    :'
    extensions.each do |e|
      label = EXTENSION_OID_NAMES[normalize_oid(e.oid)] || e.oid
      lines << "    #{label} : #{format_extension(e)}"
    end
  end

  lines.join("\n")
end

#to_s_csr_metadata(csr) ⇒ String?

Returns a formatted CSR metadata string: subject, public key, signature algorithm. Mirrors #to_s_metadata for the request side of an enrollment.

Parameters:

  • csr (OpenSSL::X509::Request, String, #to_der)

Returns:

  • (String, nil)

    nil if the CSR could not be parsed



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/msf/core/trace/certificate_trace_presenter.rb', line 188

def (csr)
  csr = self.class.coerce_csr(csr)
  return nil unless csr

  lines = [trace_separator('CSR'), "  Subject    : #{csr.subject}"]

  public_key = csr_public_key(csr)
  lines << "  Public Key : #{format_public_key(public_key)}" if public_key

  sig_alg = csr_signature_algorithm(csr)
  lines << "  Sig Alg    : #{sig_alg}" if sig_alg

  lines.join("\n")
rescue StandardError
  nil
end

#to_s_full(label: 'x.509') ⇒ String?

Returns a formatted full string: metadata + serial, version, public key algorithm, SAN / EKU / Key Usage as named fields, then remaining extensions.

Parameters:

  • label (String) (defaults to: 'x.509')

    separator header label (see #to_s_metadata)

Returns:

  • (String, nil)

    nil if certificate could not be parsed



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/msf/core/trace/certificate_trace_presenter.rb', line 146

def to_s_full(label: 'x.509')
  base = (label: label)
  return nil unless base

  lines = [base]
  lines << "  Serial     : #{@cert.serial}"
  # OpenSSL exposes the zero-based encoded X.509 version (v3 == 2).
  lines << "  Version    : v#{@cert.version + 1}"
  lines << "  Public Key : #{format_public_key(@cert.public_key)}"

  identities = parse_san_identities
  identity_key, identity_label = IDENTITY_SOURCES.find { |key, _| identities[key] }
  identity_value = identity_key ? identities[identity_key] : subject_cn
  identity_source = identity_key ? identity_label : 'Subject CN'
  lines << "  Identity   : #{identity_value} (#{identity_source})" if identity_value

  san = extension_value('subjectAltName')
  lines << "  SAN        : #{san}" if san

  eku = extension_value('extendedKeyUsage')
  lines << "  EKU        : #{eku}" if eku

  ku = extension_value('keyUsage')
  lines << "  Key Usage  : #{ku}" if ku

  other = @cert.extensions.reject { |e| NAMED_OIDS.include?(e.oid) }
  if other.any?
    lines << '  Extensions :'
    other.each do |e|
      label = EXTENSION_OID_NAMES[normalize_oid(e.oid)] || e.oid
      lines << "    #{label} : #{format_extension(e)}"
    end
  end

  lines.join("\n")
end

#to_s_metadata(label: 'x.509') ⇒ String?

Returns a formatted metadata string: subject, issuer, validity, SHA-256 fingerprint.

Parameters:

  • label (String) (defaults to: 'x.509')

    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)

Returns:

  • (String, nil)

    nil if certificate could not be parsed



126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/msf/core/trace/certificate_trace_presenter.rb', line 126

def (label: 'x.509')
  return nil unless @cert

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

  [
    trace_separator(label),
    "  Subject    : #{@cert.subject}",
    "  Issuer     : #{@cert.issuer}",
    "  Not Before : #{@cert.not_before}",
    "  Not After  : #{@cert.not_after}",
    "  SHA-256    : #{fingerprint}"
  ].join("\n")
end