From 4d02f92fabfa40cac86abd167e552b35e47df6ce Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 6 Apr 2026 19:17:38 -0400 Subject: [PATCH] Consolidate the attribute creation --- lib/msf/core/exploit/remote/cert_request.rb | 21 +++++- .../exploit/remote/http/web_enrollment.rb | 8 +- lib/msf/core/exploit/remote/ms_icpr.rb | 21 +----- .../core/exploit/remote/cert_request_spec.rb | 74 +++++++++++++++++++ .../remote/http/web_enrollment_spec.rb | 18 +++-- .../msf/core/exploit/remote/ms_icpr_spec.rb | 59 ++------------- 6 files changed, 117 insertions(+), 84 deletions(-) diff --git a/lib/msf/core/exploit/remote/cert_request.rb b/lib/msf/core/exploit/remote/cert_request.rb index 38c11bc8f3..47f5a1baf0 100644 --- a/lib/msf/core/exploit/remote/cert_request.rb +++ b/lib/msf/core/exploit/remote/cert_request.rb @@ -21,7 +21,9 @@ module Msf # @option opts [Array] :add_cert_app_policy application policy OIDs to embed # @option opts [OpenSSL::PKCS12] :pkcs12 agent certificate used to sign an on-behalf-of request # @option opts [String] :on_behalf_of UPN of the subject to request a certificate on behalf of - # @return [Array(Rex::Proto::X509::Request, OpenSSL::PKey::RSA)] the signed CSR and the private key used to sign it; + # @option opts [String] :cert_template the certificate template to request (default: CERT_TEMPLATE datastore option) + # @return [Array(Rex::Proto::X509::Request, OpenSSL::PKey::RSA, Hash)] the signed CSR, the private key used to sign + # it, and a hash of enrollment request attributes (e.g. +CertificateTemplate+, +SAN+); # when both +:pkcs12+ and +:on_behalf_of+ are supplied the first element is a # {Rex::Proto::CryptoAsn1::Cms::ContentInfo} wrapping the inner CMC request instead def create_csr(opts={}) @@ -34,16 +36,18 @@ module Msf end user = opts[:username] - status_msg = "Requesting a certificate for user #{user}" + status_msg = "Building a certificate request for user #{user}" status_msg << " - RSA key size: #{rsa_key_size}" alt_dns = opts.fetch(:alt_dns) { datastore['ALT_DNS'].blank? ? nil : datastore['ALT_DNS'] } alt_sid = opts.fetch(:alt_sid) { datastore['ALT_SID'].blank? ? nil : datastore['ALT_SID'] } alt_upn = opts.fetch(:alt_upn) { datastore['ALT_UPN'].blank? ? nil : datastore['ALT_UPN'] } algorithm = opts.fetch(:algorithm) { datastore['DigestAlgorithm'].blank? ? 'SHA256' : datastore['DigestAlgorithm'] } application_policies = opts.fetch(:add_cert_app_policy) { datastore['ADD_CERT_APP_POLICY'].blank? ? nil : datastore['ADD_CERT_APP_POLICY'].split(/[;,]\s*|\s+/) } + cert_template = opts.fetch(:cert_template) { datastore['CERT_TEMPLATE'].blank? ? nil : datastore['CERT_TEMPLATE'] } status_msg << " - alternate DNS: #{alt_dns}" if alt_dns status_msg << " - alternate UPN: #{alt_upn}" if alt_upn status_msg << " - digest algorithm: #{algorithm}" if algorithm + status_msg << " - template: #{cert_template}" if cert_template csr = Rex::Proto::X509::Request.build_csr( cn: user, private_key: private_key, @@ -75,7 +79,18 @@ module Msf end vprint_status status_msg - [csr, private_key] + attributes = {} + attributes['CertificateTemplate'] = cert_template if cert_template + san = [] + san << "dns=#{alt_dns}" if alt_dns + san << "upn=#{alt_upn}" if alt_upn + if alt_sid + san << "url=#{Rex::Proto::X509::SAN_URL_PREFIX}#{alt_sid}" + san << "url=#{alt_sid}" + end + attributes['SAN'] = san.join('&') unless san.empty? + + [csr, private_key, attributes] end end end diff --git a/lib/msf/core/exploit/remote/http/web_enrollment.rb b/lib/msf/core/exploit/remote/http/web_enrollment.rb index c3feb8991d..c019fbcf0d 100644 --- a/lib/msf/core/exploit/remote/http/web_enrollment.rb +++ b/lib/msf/core/exploit/remote/http/web_enrollment.rb @@ -52,10 +52,10 @@ module Msf def retrieve_cert(target_ip, authenticated_connection, connection_identity, cert_template) vprint_status("Creating certificate request for #{connection_identity} using the #{cert_template} template") - opts = { username: connection_identity.split('\\').last } - csr, private_key = create_csr(opts) + opts = { username: connection_identity.split('\\').last, cert_template: cert_template } + csr, private_key, attributes = create_csr(opts) - cert_template_string = "CertificateTemplate:#{cert_template}" + cert_attrib = attributes.map { |k, v| "#{k}:#{v}" }.join("\n") vprint_status('Requesting target generate certificate...') res = send_request_raw( { @@ -66,7 +66,7 @@ module Msf 'vars_post' => { 'Mode' => 'newreq', 'CertRequest' => Rex::Text.encode_base64(csr.to_der.to_s), - 'CertAttrib' => cert_template_string, + 'CertAttrib' => cert_attrib, 'TargetStoreFlags' => 0, 'SaveCert' => 'yes', 'ThumbPrint' => '' diff --git a/lib/msf/core/exploit/remote/ms_icpr.rb b/lib/msf/core/exploit/remote/ms_icpr.rb index 786e7b272d..2c829346c0 100644 --- a/lib/msf/core/exploit/remote/ms_icpr.rb +++ b/lib/msf/core/exploit/remote/ms_icpr.rb @@ -104,28 +104,9 @@ module Exploit::Remote::MsIcpr # and different opts hash values, so we need this here to make # sure all the data we need is populated opts[:username] = opts.fetch(:username) { datastore['SMBUser'] } - status_msg = "Requesting a certificate for user #{opts[:username]}" - alt_dns = opts[:alt_dns] || (datastore['ALT_DNS'].blank? ? nil : datastore['ALT_DNS']) - alt_sid = opts[:alt_sid] || (datastore['ALT_SID'].blank? ? nil : datastore['ALT_SID']) - alt_upn = opts[:alt_upn] || (datastore['ALT_UPN'].blank? ? nil : datastore['ALT_UPN']) application_policies = opts[:add_cert_app_policy] || (datastore['ADD_CERT_APP_POLICY'].blank? ? nil : datastore['ADD_CERT_APP_POLICY'].split(/[;,]\s*|\s+/)) - csr, private_key = create_csr(opts) - cert_template = opts[:cert_template] || datastore['CERT_TEMPLATE'] - status_msg << " - template: #{cert_template}" - attributes = { 'CertificateTemplate' => cert_template } - san = [] - san << "dns=#{alt_dns}" if alt_dns - san << "upn=#{alt_upn}" if alt_upn - - if alt_sid - san << "url=#{SAN_URL_PREFIX}#{alt_sid}" - san << "url=#{alt_sid}" - end - - attributes['SAN'] = san.join('&') unless san.empty? - - vprint_status(status_msg) + csr, private_key, attributes = create_csr(opts) response = icpr.cert_server_request( attributes: attributes, diff --git a/spec/lib/msf/core/exploit/remote/cert_request_spec.rb b/spec/lib/msf/core/exploit/remote/cert_request_spec.rb index 7c67f8cda1..98ea0dffb7 100644 --- a/spec/lib/msf/core/exploit/remote/cert_request_spec.rb +++ b/spec/lib/msf/core/exploit/remote/cert_request_spec.rb @@ -362,5 +362,79 @@ RSpec.describe Msf::Exploit::Remote::CertRequest do subject.create_csr(username: 'alice', private_key: rsa_key) end end + + context 'returned attributes' do + context 'with cert_template supplied via opts' do + it 'sets CertificateTemplate in the returned attributes' do + _csr, _key, attrs = subject.create_csr(username: 'alice', private_key: rsa_key, cert_template: 'AdminTemplate') + expect(attrs['CertificateTemplate']).to eq('AdminTemplate') + end + end + + context 'with cert_template supplied via datastore' do + before { subject.datastore['CERT_TEMPLATE'] = 'UserTemplate' } + + it 'sets CertificateTemplate from the datastore' do + _csr, _key, attrs = subject.create_csr(username: 'alice', private_key: rsa_key) + expect(attrs['CertificateTemplate']).to eq('UserTemplate') + end + end + + context 'when no cert_template is supplied' do + it 'does not include CertificateTemplate in the returned attributes' do + _csr, _key, attrs = subject.create_csr(username: 'alice', private_key: rsa_key) + expect(attrs).not_to have_key('CertificateTemplate') + end + end + + context 'with alt_dns supplied via opts' do + it 'includes dns in the SAN attribute' do + _csr, _key, attrs = subject.create_csr(username: 'alice', private_key: rsa_key, alt_dns: 'host.example.com') + expect(attrs['SAN']).to include('dns=host.example.com') + end + end + + context 'with alt_upn supplied via opts' do + it 'includes upn in the SAN attribute' do + _csr, _key, attrs = subject.create_csr(username: 'alice', private_key: rsa_key, alt_upn: 'alice@example.com') + expect(attrs['SAN']).to include('upn=alice@example.com') + end + end + + context 'with alt_sid supplied via opts' do + let(:sid) { 'S-1-5-21-1234567890-1234567890-1234567890-500' } + + before { allow(Rex::Proto::X509::Request).to receive(:build_csr).and_return(double('csr', to_der: '')) } + + it 'includes the prefixed URL SAN entry in the returned attributes' do + _csr, _key, attrs = subject.create_csr(username: 'alice', private_key: rsa_key, alt_sid: sid) + expect(attrs['SAN']).to include("url=#{Rex::Proto::X509::SAN_URL_PREFIX}#{sid}") + end + + it 'includes the bare URL SAN entry in the returned attributes' do + _csr, _key, attrs = subject.create_csr(username: 'alice', private_key: rsa_key, alt_sid: sid) + expect(attrs['SAN']).to include("url=#{sid}") + end + end + + context 'with alt_dns and alt_upn supplied' do + it 'joins both SAN entries with &' do + _csr, _key, attrs = subject.create_csr( + username: 'alice', private_key: rsa_key, + alt_dns: 'host.example.com', alt_upn: 'alice@example.com' + ) + expect(attrs['SAN']).to include('dns=host.example.com') + expect(attrs['SAN']).to include('upn=alice@example.com') + expect(attrs['SAN']).to include('&') + end + end + + context 'when no SAN values are set' do + it 'does not include SAN in the returned attributes' do + _csr, _key, attrs = subject.create_csr(username: 'alice', private_key: rsa_key) + expect(attrs).not_to have_key('SAN') + end + end + end end end diff --git a/spec/lib/msf/core/exploit/remote/http/web_enrollment_spec.rb b/spec/lib/msf/core/exploit/remote/http/web_enrollment_spec.rb index b4e8a5cdab..b4729aa07d 100644 --- a/spec/lib/msf/core/exploit/remote/http/web_enrollment_spec.rb +++ b/spec/lib/msf/core/exploit/remote/http/web_enrollment_spec.rb @@ -211,7 +211,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::WebEnrollment do let(:res) { double('response', code: 200, body: 'Certificate request was denied by policy') } before do - allow(subject).to receive(:create_csr).and_return(csr) + allow(subject).to receive(:create_csr).and_return([csr, nil, {}]) allow(subject).to receive(:send_request_raw).and_return(res) end @@ -225,7 +225,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::WebEnrollment do let(:res) { double('response', code: 200, body: 'Certificate request failed due to an error') } before do - allow(subject).to receive(:create_csr).and_return(csr) + allow(subject).to receive(:create_csr).and_return([csr, nil, {}]) allow(subject).to receive(:send_request_raw).and_return(res) end @@ -239,7 +239,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::WebEnrollment do let(:res) { double('response', code: 401, body: 'Error: invalid credentials provided') } before do - allow(subject).to receive(:create_csr).and_return(csr) + allow(subject).to receive(:create_csr).and_return([csr, nil, {}]) allow(subject).to receive(:send_request_raw).and_return(res) end @@ -253,7 +253,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::WebEnrollment do let(:res) { double('response', code: 200, body: 'Certificate generated successfully, no location here') } before do - allow(subject).to receive(:create_csr).and_return(csr) + allow(subject).to receive(:create_csr).and_return([csr, nil, {}]) allow(subject).to receive(:send_request_raw).and_return(res) end @@ -287,7 +287,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::WebEnrollment do let(:get_res) { double('response', code: 200, body: certificate.to_der) } before do - allow(subject).to receive(:create_csr).and_return([csr, rsa_key]) + allow(subject).to receive(:create_csr).and_return([csr, rsa_key, { 'CertificateTemplate' => template }]) allow(subject).to receive(:send_request_raw).and_return(post_res, get_res) allow(subject).to receive(:store_loot).and_return('/tmp/cert.pfx') end @@ -313,6 +313,14 @@ RSpec.describe Msf::Exploit::Remote::HTTP::WebEnrollment do ) subject.retrieve_cert(target_ip, authenticated_client, identity, template) end + + it 'posts CertAttrib with key:value format' do + expect(subject).to receive(:send_request_raw).with( + hash_including('vars_post' => hash_including('CertAttrib' => "CertificateTemplate:#{template}")) + ).and_return(post_res) + allow(subject).to receive(:send_request_raw).and_return(get_res) + subject.retrieve_cert(target_ip, authenticated_client, identity, template) + end end end end diff --git a/spec/lib/msf/core/exploit/remote/ms_icpr_spec.rb b/spec/lib/msf/core/exploit/remote/ms_icpr_spec.rb index 9c1b90e0ec..edd63fe8e1 100644 --- a/spec/lib/msf/core/exploit/remote/ms_icpr_spec.rb +++ b/spec/lib/msf/core/exploit/remote/ms_icpr_spec.rb @@ -284,7 +284,7 @@ RSpec.describe Msf::Exploit::Remote::MsIcpr do let(:csr_double) { double('csr', to_der: 'der') } before do - allow(subject).to receive(:create_csr).and_return(csr_double) + allow(subject).to receive(:create_csr).and_return([csr_double, rsa_key, {}]) allow(icpr).to receive(:cert_server_request).and_return({ status: :issued }) end @@ -309,57 +309,12 @@ RSpec.describe Msf::Exploit::Remote::MsIcpr do end end - context 'certificate template' do - it 'uses cert_template from opts' do - expect(icpr).to receive(:cert_server_request).with(hash_including( - attributes: hash_including('CertificateTemplate' => 'AdminTemplate') - )) - subject.send(:do_request_cert, icpr, { username: 'alice', private_key: rsa_key, cert_template: 'AdminTemplate' }) - end - - it 'falls back to datastore CERT_TEMPLATE' do - subject.datastore['CERT_TEMPLATE'] = 'UserTemplate' - expect(icpr).to receive(:cert_server_request).with(hash_including( - attributes: hash_including('CertificateTemplate' => 'UserTemplate') - )) - subject.send(:do_request_cert, icpr, { username: 'alice', private_key: rsa_key }) - end - end - - context 'SAN attributes' do - it 'includes dns SAN when alt_dns is in opts' do - expect(icpr).to receive(:cert_server_request).with(hash_including( - attributes: hash_including('SAN' => 'dns=host.example.com') - )) - subject.send(:do_request_cert, icpr, { username: 'alice', private_key: rsa_key, alt_dns: 'host.example.com' }) - end - - it 'includes upn SAN when alt_upn is in opts' do - expect(icpr).to receive(:cert_server_request).with(hash_including( - attributes: hash_including('SAN' => 'upn=alice@example.com') - )) - subject.send(:do_request_cert, icpr, { username: 'alice', private_key: rsa_key, alt_upn: 'alice@example.com' }) - end - - it 'includes both dns and upn SANs when both are set' do - expect(icpr).to receive(:cert_server_request) do |args| - san = args[:attributes]['SAN'] - expect(san).to include('dns=host.example.com') - expect(san).to include('upn=alice@example.com') - { status: :issued } - end - subject.send(:do_request_cert, icpr, { - username: 'alice', private_key: rsa_key, - alt_dns: 'host.example.com', alt_upn: 'alice@example.com' - }) - end - - it 'omits SAN attribute entirely when no alt values are set' do - expect(icpr).to receive(:cert_server_request) do |args| - expect(args[:attributes]).not_to have_key('SAN') - { status: :issued } - end - subject.send(:do_request_cert, icpr, { username: 'alice', private_key: rsa_key }) + context 'attributes pass-through' do + it 'passes the attributes hash from create_csr directly to cert_server_request' do + attrs = { 'CertificateTemplate' => 'TestTemplate', 'SAN' => 'upn=alice@example.com' } + allow(subject).to receive(:create_csr).and_return([csr_double, rsa_key, attrs]) + expect(icpr).to receive(:cert_server_request).with(hash_including(attributes: attrs)) + subject.send(:do_request_cert, icpr, { username: 'alice' }) end end end