From d24158360dd7e566b3101ac2fe0860df36e28641 Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Mon, 16 Dec 2024 12:18:40 +0100 Subject: [PATCH 1/4] Update Pkcs12-related code to report CA and ADCS Template to the database - Update the `creds` command to add Pkcs12 private credentials with metadata. - Update `ms_icpr` module to store metadata. --- lib/msf/core/exploit/remote/ms_icpr.rb | 8 +- .../ui/console/command_dispatcher/creds.rb | 34 +++-- .../console/command_dispatcher/creds_spec.rb | 124 ++++++++++++++---- 3 files changed, 125 insertions(+), 41 deletions(-) diff --git a/lib/msf/core/exploit/remote/ms_icpr.rb b/lib/msf/core/exploit/remote/ms_icpr.rb index 925baa796dd0..ae4fbcf80b3f 100644 --- a/lib/msf/core/exploit/remote/ms_icpr.rb +++ b/lib/msf/core/exploit/remote/ms_icpr.rb @@ -232,8 +232,12 @@ def do_request_cert(icpr, opts) workspace_id: myworkspace_id, username: upn || datastore['SMBUser'], private_type: :pkcs12, - # pkcs12 is a binary format, but for persisting we Base64 encode it - private_data: Base64.strict_encode64(pkcs12.to_der), + private_data: Metasploit::Credential::Pkcs12.build_data( + # pkcs12 is a binary format, but for persisting we Base64 encode it + pkcs12: Base64.strict_encode64(pkcs12.to_der), + ca: datastore['CA'], + adcs_template: cert_template + ), origin_type: :service, module_fullname: fullname } diff --git a/lib/msf/ui/console/command_dispatcher/creds.rb b/lib/msf/ui/console/command_dispatcher/creds.rb index 220365a4c70f..2a165e621c5c 100644 --- a/lib/msf/ui/console/command_dispatcher/creds.rb +++ b/lib/msf/ui/console/command_dispatcher/creds.rb @@ -100,16 +100,18 @@ def cmd_creds_help print_line "Usage - Adding credentials:" print_line " creds add uses the following named parameters." { - user: 'Public, usually a username', - password: 'Private, private_type Password.', - ntlm: 'Private, private_type NTLM Hash.', - postgres: 'Private, private_type postgres MD5', - pkcs12: 'Private, private_type pkcs12 archive file, must be a file path.', - 'ssh-key' => 'Private, private_type SSH key, must be a file path.', - hash: 'Private, private_type Nonreplayable hash', - jtr: 'Private, private_type John the Ripper hash type.', - realm: 'Realm, ', - 'realm-type'=>"Realm, realm_type (#{Metasploit::Model::Realm::Key::SHORT_NAMES.keys.join(' ')}), defaults to domain." + user: 'Public, usually a username', + password: 'Private, private_type Password.', + ntlm: 'Private, private_type NTLM Hash.', + postgres: 'Private, private_type postgres MD5', + pkcs12: 'Private, private_type pkcs12 archive file, must be a file path.', + 'ssh-key' => 'Private, private_type SSH key, must be a file path.', + hash: 'Private, private_type Nonreplayable hash', + jtr: 'Private, private_type John the Ripper hash type.', + realm: 'Realm, ', + 'realm-type' => "Realm, realm_type (#{Metasploit::Model::Realm::Key::SHORT_NAMES.keys.join(' ')}), defaults to domain.", + ca: 'CA, Certificate Authority that issued the pkcs12 certificate', + 'adcs-template' => 'ADCS Template, template used to issue the pkcs12 certificate' }.each_pair do |keyword, description| print_line " #{keyword.to_s.ljust 10}: #{description}" end @@ -206,7 +208,7 @@ def creds_add(*args) end begin - params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','address','port','protocol', 'service-name', 'jtr', 'pkcs12', 'postgres') + params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','address','port','protocol', 'service-name', 'jtr', 'pkcs12', 'postgres', 'ca', 'adcs-template') rescue ArgumentError => e print_error(e.message) end @@ -275,7 +277,11 @@ def creds_add(*args) print_error("Failed to add pkcs12 archive: #{e}") end data[:private_type] = :pkcs12 - data[:private_data] = pkcs12_data + data[:private_data] = Metasploit::Credential::Pkcs12.build_data( + pkcs12: pkcs12_data, + ca: params['ca'], + adcs_template: params['adcs-template'] + ) end if params.key? 'hash' @@ -414,11 +420,13 @@ def creds_search(*args) when 'password' Metasploit::Credential::Password when 'hash' - Metasploit::Credential::PasswordHash + Metasploit::Credential::NonreplayableHash when 'ntlm' Metasploit::Credential::NTLMHash when 'KrbEncKey'.downcase Metasploit::Credential::KrbEncKey + when 'pkcs12' + Metasploit::Credential::Pkcs12 when *Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS opts[:jtr_format] = ptype Metasploit::Credential::NonreplayableHash diff --git a/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb index 7e08644e83bf..67dcef8d3faf 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb @@ -212,32 +212,48 @@ realm: nil, workspace: framework.db.workspace) end + let!(:pkcs12_subject) { '/C=FR/O=MyOrg/OU=MyUnit/CN=SubjectTestName' } + let!(:pkcs12_issuer) { '/C=US/O=MyIssuer/OU=MyIssuerUnit/CN=IssuerTestName' } + let!(:pkcs12_ca) { 'testCA' } + let!(:pkcs12_adcs_template) { 'TestTemplate' } + let!(:pkcs12_core) do + priv = FactoryBot.create(:metasploit_credential_pkcs12_with_ca_and_adcs_template, + subject: pkcs12_subject, + issuer: pkcs12_issuer, + ca: pkcs12_ca, + adcs_template: pkcs12_adcs_template) + FactoryBot.create(:metasploit_credential_core, + origin: FactoryBot.create(:metasploit_credential_origin_import), + private: priv, + public: nil, + realm: nil, + workspace: framework.db.workspace) + end - # # Somehow this is hitting a unique constraint on Cores with the same - # # Public, even though it has a different Private. Skip for now - # let!(:ntlm_core) do - # priv = FactoryBot.create(:metasploit_credential_ntlm_hash, data: ntlm_hash) - # FactoryBot.create(:metasploit_credential_core, - # origin: FactoryBot.create(:metasploit_credential_origin_import), - # private: priv, - # public: pub, - # realm: nil, - # workspace: framework.db.workspace) - # end - # let!(:nonreplayable_core) do - # priv = FactoryBot.create(:metasploit_credential_nonreplayable_hash, data: 'asdf') - # FactoryBot.create(:metasploit_credential_core, - # origin: FactoryBot.create(:metasploit_credential_origin_import), - # private: priv, - # public: pub, - # realm: nil, - # workspace: framework.db.workspace) - # end + let!(:ntlm_core) do + priv = FactoryBot.create(:metasploit_credential_ntlm_hash, data: ntlm_hash) + FactoryBot.create(:metasploit_credential_core, + origin: FactoryBot.create(:metasploit_credential_origin_import), + private: priv, + public: pub, + realm: nil, + workspace: framework.db.workspace) + end + let!(:nonreplayable_core) do + priv = FactoryBot.create(:metasploit_credential_nonreplayable_hash, data: 'asdf') + FactoryBot.create(:metasploit_credential_core, + origin: FactoryBot.create(:metasploit_credential_origin_import), + private: priv, + public: pub, + realm: nil, + workspace: framework.db.workspace) + end after(:example) do - # ntlm_core.destroy + ntlm_core.destroy password_core.destroy - # nonreplayable_core.destroy + nonreplayable_core.destroy + pkcs12_core.destroy end context 'password' do @@ -283,16 +299,48 @@ context 'ntlm' do it 'should show just the ntlm' do - skip 'Weird uniqueness constraint on Core (workspace_id, public_id)' creds.cmd_creds('-t', 'ntlm') expect(@output.join("\n")).to match_table <<~TABLE Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - thisuser 1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c NTLM hash + host origin service public private realm private_type JtR Format cracked_password + ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + thisuser 1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c NTLM hash + + TABLE + end + end + + context 'nonreplayable' do + it 'should show just the ntlm' do + + creds.cmd_creds('-t', 'hash') + expect(@output.join("\n")).to match_table <<~TABLE + Credentials + =========== + + host origin service public private realm private_type JtR Format cracked_password + ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + thisuser asdf Nonreplayable hash + + TABLE + end + end + + context 'pkcs12' do + it 'should show just the pkcs12' do + private_str = "subject:#{pkcs12_subject},issuer:#{pkcs12_issuer},CA:#{pkcs12_ca},ADCS_template:#{pkcs12_adcs_template}" + private_str = "#{private_str[0,76]} (TRUNCATED)" + creds.cmd_creds('-t', 'pkcs12') + expect(@output.join("\n")).to match_table <<~TABLE + Credentials + =========== + + host origin service public private realm private_type JtR Format cracked_password + ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + #{private_str} Pkcs12 (pfx) TABLE end @@ -479,6 +527,30 @@ }.to_not change { Metasploit::Credential::Core.count } end end + context 'pkcs12' do + let(:priv) { FactoryBot.create(:metasploit_credential_pkcs12) } + before(:each) do + @file = Tempfile.new('mypkcs12.pfx') + @file.write(Base64.strict_decode64(priv.pkcs12)) + @file.close + end + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "pkcs12:#{@file.path}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryBot.create(:metasploit_credential_core, + origin: FactoryBot.create(:metasploit_credential_origin_import), + private: priv, + public: nil, + realm: nil, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "pkcs12:#{@file.path}") + }.to_not change { Metasploit::Credential::Core.count } + end + end end context 'realm-types' do Metasploit::Model::Realm::Key::SHORT_NAMES.each do |short_name, long_name| From 96f7538d86e32960eb074c5e23ead979e85111c4 Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Mon, 16 Dec 2024 15:06:35 +0100 Subject: [PATCH 2/4] Point Gemfile to the metasploit-credentials feature branch on cdelafuente-r7 repo --- Gemfile | 2 ++ Gemfile.lock | 27 +++++++++++++++++---------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 83b7b2811fbd..391cff021bc8 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,8 @@ source 'https://rubygems.org' # spec.add_runtime_dependency '', [] gemspec name: 'metasploit-framework' +gem 'metasploit-credential', git: 'git@github.com:cdelafuente-r7/metasploit-credential.git', branch: 'enh/MS-9710/add_pkcs12_metadata' + # separate from test as simplecov is not run on travis-ci group :coverage do # code coverage for tests diff --git a/Gemfile.lock b/Gemfile.lock index 4163cd5f8163..887243188ced 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,19 @@ +GIT + remote: git@github.com:cdelafuente-r7/metasploit-credential.git + revision: acc5a012f4bc7e7774af059e778b947cd994da1e + branch: enh/MS-9710/add_pkcs12_metadata + specs: + metasploit-credential (6.0.12) + metasploit-concern + metasploit-model + metasploit_data_models (>= 5.0.0) + net-ssh + pg + railties + rex-socket + rubyntlm + rubyzip + PATH remote: . specs: @@ -286,16 +302,6 @@ GEM activesupport (~> 7.0) railties (~> 7.0) zeitwerk - metasploit-credential (6.0.11) - metasploit-concern - metasploit-model - metasploit_data_models (>= 5.0.0) - net-ssh - pg - railties - rex-socket - rubyntlm - rubyzip metasploit-model (5.0.2) activemodel (~> 7.0) activesupport (~> 7.0) @@ -581,6 +587,7 @@ DEPENDENCIES factory_bot_rails fivemat memory_profiler + metasploit-credential! metasploit-framework! octokit pry-byebug From 4c5a3657159c9ad59246d3450cde001c5ff82b28 Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Mon, 16 Dec 2024 15:49:18 +0100 Subject: [PATCH 3/4] Update Gemfile to use https --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 391cff021bc8..a6a9ebadd26e 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' # spec.add_runtime_dependency '', [] gemspec name: 'metasploit-framework' -gem 'metasploit-credential', git: 'git@github.com:cdelafuente-r7/metasploit-credential.git', branch: 'enh/MS-9710/add_pkcs12_metadata' +gem 'metasploit-credential', git: 'https://github.com/cdelafuente-r7/metasploit-credential', branch: 'enh/MS-9710/add_pkcs12_metadata' # separate from test as simplecov is not run on travis-ci group :coverage do diff --git a/Gemfile.lock b/Gemfile.lock index 887243188ced..82199bed42a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,5 +1,5 @@ GIT - remote: git@github.com:cdelafuente-r7/metasploit-credential.git + remote: https://github.com/cdelafuente-r7/metasploit-credential revision: acc5a012f4bc7e7774af059e778b947cd994da1e branch: enh/MS-9710/add_pkcs12_metadata specs: From c4d21eb465ea56a816003636c2f3fa2ebae840a3 Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Fri, 20 Dec 2024 18:34:09 +0100 Subject: [PATCH 4/4] Add certs command & use pkinit if kerberos tickets are not available in cache --- lib/metasploit/framework/ldap/client.rb | 33 +++- .../framework/login_scanner/ldap.rb | 4 +- .../kerberos/service_authenticator/base.rb | 11 ++ lib/msf/core/exploit/remote/ms_icpr.rb | 7 +- lib/msf/core/exploit/remote/pkcs12/storage.rb | 86 ++++++++++ .../exploit/remote/pkcs12/stored_pkcs12.rb | 38 +++++ lib/msf/ui/console/command_dispatcher/db.rb | 2 + .../ui/console/command_dispatcher/db/certs.rb | 154 ++++++++++++++++++ modules/auxiliary/scanner/ldap/ldap_login.rb | 8 + modules/auxiliary/scanner/smb/smb_login.rb | 13 +- 10 files changed, 344 insertions(+), 12 deletions(-) create mode 100644 lib/msf/core/exploit/remote/pkcs12/storage.rb create mode 100644 lib/msf/core/exploit/remote/pkcs12/stored_pkcs12.rb create mode 100644 lib/msf/ui/console/command_dispatcher/db/certs.rb diff --git a/lib/metasploit/framework/ldap/client.rb b/lib/metasploit/framework/ldap/client.rb index 1c1c038c0d94..57d9a5696478 100644 --- a/lib/metasploit/framework/ldap/client.rb +++ b/lib/metasploit/framework/ldap/client.rb @@ -50,6 +50,7 @@ def ldap_auth_opts_kerberos(opts, ssl) auth_opts = {} raise Msf::ValidationError, 'The LDAP::Rhostname option is required when using Kerberos authentication.' if opts[:ldap_rhostname].blank? raise Msf::ValidationError, 'The DOMAIN option is required when using Kerberos authentication.' if opts[:domain].blank? + raise Msf::ValidationError, 'The DomainControllerRhost is required when using Kerberos authentication.' if opts[:domain_controller_rhost].blank? offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(opts[:ldap_krb_offered_enc_types]) raise Msf::ValidationError, 'At least one encryption type is required when using Kerberos authentication.' if offered_etypes.empty? @@ -112,17 +113,33 @@ def ldap_auth_opts_schannel(opts, ssl) auth_opts = {} pfx_path = opts[:ldap_cert_file] raise Msf::ValidationError, 'The SSL option must be enabled when using Schannel authentication.' unless ssl - raise Msf::ValidationError, 'The LDAP::CertFile option is required when using Schannel authentication.' if pfx_path.blank? raise Msf::ValidationError, 'Can not sign and seal when using Schannel authentication.' if opts.fetch(:sign_and_seal, false) - unless ::File.file?(pfx_path) && ::File.readable?(pfx_path) - raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.' - end + if pfx_path.present? + unless ::File.file?(pfx_path) && ::File.readable?(pfx_path) + raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.' + end + + begin + pkcs = OpenSSL::PKCS12.new(File.binread(pfx_path), '') + rescue StandardError => e + raise Msf::ValidationError, "Failed to load the PFX file (#{e})" + end + else + pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new( + framework: opts[:framework], + framework_module: opts[:framework_module] + ) + pkcs12_results = pkcs12_storage.pkcs12( + username: opts[:username], + realm: opts[:domain] + ) + if pkcs12_results.empty? + raise Msf::ValidationError, "Pkcs12 for #{opts[:username]}@#{opts[:domain]} not found in the database" + end - begin - pkcs = OpenSSL::PKCS12.new(File.binread(pfx_path), '') - rescue StandardError => e - raise Msf::ValidationError, "Failed to load the PFX file (#{e})" + elog("Using stored certificate for #{opts[:username]}@#{opts[:domain]}") + pkcs = pkcs12_results.first.openssl_pkcs12 end auth_opts[:auth] = { diff --git a/lib/metasploit/framework/login_scanner/ldap.rb b/lib/metasploit/framework/login_scanner/ldap.rb index ef0ae8d63076..38872f7c74e5 100644 --- a/lib/metasploit/framework/login_scanner/ldap.rb +++ b/lib/metasploit/framework/login_scanner/ldap.rb @@ -86,8 +86,8 @@ def each_credential credential.private = nil elsif opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::SCHANNEL # If we're using kerberos auth with schannel then the user/password is irrelevant - # Remove it from the credential so we don't store it - credential.public = nil + # Remove the password from the credential so we don't store it + # Note that the username is kept since it is needed for the certificate lookup. credential.private = nil end diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb index daef70e9fb63..be2d74efa1eb 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -253,6 +253,17 @@ def authenticate(options = {}) elsif options[:credential] auth_context = authenticate_via_krb5_ccache_credential_tgs(options[:credential], options) else + pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework, framework_module: framework_module) + pkcs12_results = pkcs12_storage.pkcs12( + workspace: workspace, + username: @username, + realm: @realm + ) + if pkcs12_results.any? + stored_pkcs12 = pkcs12_results.first + options[:pfx] = stored_pkcs12.openssl_pkcs12 + print_status("Using stored certificate for #{stored_pkcs12.username}@#{stored_pkcs12.realm}") + end auth_context = authenticate_via_kdc(options) auth_context = authenticate_via_krb5_ccache_credential_tgt(auth_context[:credential], options) end diff --git a/lib/msf/core/exploit/remote/ms_icpr.rb b/lib/msf/core/exploit/remote/ms_icpr.rb index ae4fbcf80b3f..f6daacf02898 100644 --- a/lib/msf/core/exploit/remote/ms_icpr.rb +++ b/lib/msf/core/exploit/remote/ms_icpr.rb @@ -221,6 +221,9 @@ def do_request_cert(icpr, opts) pkcs12 = OpenSSL::PKCS12.create('', '', private_key, response[:certificate]) # see: https://pki-tutorial.readthedocs.io/en/latest/mime.html#mime-types info = "#{simple.client.default_domain}\\#{datastore['SMBUser']} Certificate" + # TODO: I was under the impression a single certificate can only have one UPN associated with it. + # But here, `upn` can be an array of UPN's. This will need to be sorted out. + upn_username, upn_domain = upn&.first&.split('@') service_data = icpr_service_data credential_data = { @@ -230,7 +233,7 @@ def do_request_cert(icpr, opts) protocol: service_data[:proto], service_name: service_data[:name], workspace_id: myworkspace_id, - username: upn || datastore['SMBUser'], + username: upn_username || datastore['SMBUser'], private_type: :pkcs12, private_data: Metasploit::Credential::Pkcs12.build_data( # pkcs12 is a binary format, but for persisting we Base64 encode it @@ -238,6 +241,8 @@ def do_request_cert(icpr, opts) ca: datastore['CA'], adcs_template: cert_template ), + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: upn_domain || simple.client.default_domain, origin_type: :service, module_fullname: fullname } diff --git a/lib/msf/core/exploit/remote/pkcs12/storage.rb b/lib/msf/core/exploit/remote/pkcs12/storage.rb new file mode 100644 index 000000000000..c89931100402 --- /dev/null +++ b/lib/msf/core/exploit/remote/pkcs12/storage.rb @@ -0,0 +1,86 @@ +module Msf::Exploit::Remote::Pkcs12 + + class Storage + include Msf::Auxiliary::Report + + # @!attribute [r] framework + # @return [Msf::Framework] the Metasploit framework instance + attr_reader :framework + + # @!attribute [r] framework_module + # @return [Msf::Module] the Metasploit framework module that is associated with the authentication instance + attr_reader :framework_module + + def initialize(framework: nil, framework_module: nil) + @framework = framework || framework_module&.framework + @framework_module = framework_module + end + + # Get stored pkcs12 matching the options query. + # + # @param [Hash] options The options for matching pkcs12's. + # @option options [Integer, Array] :id The identifier of the pkcs12 (optional) + # @option options [String] :realm The realm of the pkcs12 (optional) + # @option options [String] :username The username of the pkcs12 (optional) + # @return [Array] + def pkcs12(options = {}, &block) + stored_pkcs12_array = filter_pkcs12(options).map do |pkcs12_entry| + StoredPkcs12.new(pkcs12_entry) + end + + stored_pkcs12_array.each do |stored_pkcs12| + block.call(stored_pkcs12) if block_given? + end + + stored_pkcs12_array + end + + # Return the raw stored pkcs12. + # + # @param [Hash] options See the options hash description in {#pkcs12}. + # @return [Array] + def filter_pkcs12(options) + return [] unless active_db? + + filter = {} + filter[:id] = options[:id] if options[:id].present? + filter[:user] = options[:username] if options[:username].present? + filter[:realm] = options[:realm] if options[:realm].present? + + creds = framework.db.creds( + workspace: options.fetch(:workspace) { workspace }, + type: 'Metasploit::Credential::Pkcs12', + **filter + ).select do |cred| + cred.private.type == 'Metasploit::Credential::Pkcs12' + end + + creds.each do |stored_cred| + block.call(stored_cred) if block_given? + end + end + + def delete_pkcs12(options = {}) + if options.keys == [:ids] + # skip calling #filter_pkcs12 which issues a query when the IDs are specified + ids = options[:ids] + else + ids = filter_pkcs12(options).map(&:id) + end + + framework.db.delete_credentials(ids: ids).map do |stored_pkcs12| + StoredPkcs12.new(stored_pkcs12) + end + end + + # @return [String] The name of the workspace in which to operate. + def workspace + if @framework_module + return @framework_module.workspace + elsif @framework&.db&.active + return @framework.db.workspace&.name + end + end + + end +end diff --git a/lib/msf/core/exploit/remote/pkcs12/stored_pkcs12.rb b/lib/msf/core/exploit/remote/pkcs12/stored_pkcs12.rb new file mode 100644 index 000000000000..d2c70ca01b69 --- /dev/null +++ b/lib/msf/core/exploit/remote/pkcs12/stored_pkcs12.rb @@ -0,0 +1,38 @@ +module Msf::Exploit::Remote::Pkcs12 + + class StoredPkcs12 + def initialize(pkcs12) + @pkcs12 = pkcs12 + end + + def id + @pkcs12.id + end + + def openssl_pkcs12 + private_cred.openssl_pkcs12 + end + + def ca + private_cred.ca + end + + def adcs_template + private_cred.adcs_template + end + + def private_cred + @pkcs12.private + end + + def username + @pkcs12.public.username + end + + def realm + @pkcs12.realm.value + end + end + +end + diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 711cae3c8cec..8cf44e11e9aa 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -19,6 +19,7 @@ class Db include Msf::Ui::Console::CommandDispatcher::Db::Common include Msf::Ui::Console::CommandDispatcher::Db::Analyze include Msf::Ui::Console::CommandDispatcher::Db::Klist + include Msf::Ui::Console::CommandDispatcher::Db::Certs DB_CONFIG_PATH = 'framework/database' @@ -49,6 +50,7 @@ def commands "notes" => "List all notes in the database", "loot" => "List all loot in the database", "klist" => "List Kerberos tickets in the database", + "certs" => "List Pkcs12 certificate bundles in the database", "db_import" => "Import a scan result file (filetype will be auto-detected)", "db_export" => "Export a file containing the contents of the database", "db_nmap" => "Executes nmap and records the output automatically", diff --git a/lib/msf/ui/console/command_dispatcher/db/certs.rb b/lib/msf/ui/console/command_dispatcher/db/certs.rb new file mode 100644 index 000000000000..adb99db2eb3d --- /dev/null +++ b/lib/msf/ui/console/command_dispatcher/db/certs.rb @@ -0,0 +1,154 @@ +# -*- coding: binary -*- + +module Msf::Ui::Console::CommandDispatcher::Db::Certs + # + # Tab completion for the certs command + # + # @param str [String] the string currently being typed before tab was hit + # @param words [Array] the previously completed words on the command line. words is always + # at least 1 when tab completion has reached this stage since the command itself has been completed + def cmd_certs_tabs(str, words) + if words.length == 1 + @@certs_opts.option_keys.select { |opt| opt.start_with?(str) } + end + end + + def cmd_certs_help + print_line 'List Pkcs12 certificate bundles in the database' + print_line 'Usage: certs [options] [username[@domain_upn_format]]' + print_line + print @@certs_opts.usage + print_line + end + + @@certs_opts = Rex::Parser::Arguments.new( + ['-v', '--verbose'] => [false, 'Verbose output'], + ['-d', '--delete'] => [ false, 'Delete *all* matching pkcs12 entries'], + ['-h', '--help'] => [false, 'Help banner'], + ['-i', '--index'] => [true, 'Pkcs12 entry ID(s) to search for, e.g. `-i 1` or `-i 1,2,3` or `-i 1 -i 2 -i 3`'], + ) + + def cmd_certs(*args) + return unless active? + + entries_affected = 0 + mode = :list + id_search = [] + username = nil + verbose = false + @@certs_opts.parse(args) do |opt, _idx, val| + case opt + when '-h', '--help' + cmd_certs_help + return + when '-v', '--verbose' + verbose = true + when '-d', '--delete' + mode = :delete + when '-i', '--id' + id_search = (id_search + val.split(/,\s*|\s+/)).uniq # allows 1 or 1,2,3 or "1 2 3" or "1, 2, 3" + else + # Anything that wasn't an option is a username to search for + username = val + end + end + + pkcs12_results = pkcs12_search(username: username, id_search: id_search) + + print_line('Pkcs12') + print_line('======') + + if mode == :delete + result = pkcs12_storage.delete_pkcs12(ids: pkcs12_results.map(&:id)) + entries_affected = result.size + end + + if pkcs12_results.empty? + print_line('No Pkcs12') + print_line + return + end + + if verbose + pkcs12_results.each.with_index do |pkcs12_result, index| + print_line "Certificate[#{index}]:" + print_line pkcs12_result.openssl_pkcs12.certificate.to_s + print_line pkcs12_result.openssl_pkcs12.certificate.to_text + print_line + end + else + tbl = Rex::Text::Table.new( + { + 'Columns' => ['id', 'username', 'realm', 'subject', 'issuer', 'CA', 'ADCS Template'], + 'SortIndex' => -1, + 'WordWrap' => false, + 'Rows' => pkcs12_results.map do |pkcs12| + [ + pkcs12.id, + pkcs12.username, + pkcs12.realm, + pkcs12.openssl_pkcs12.certificate.subject.to_s, + pkcs12.openssl_pkcs12.certificate.issuer.to_s, + pkcs12.ca, + pkcs12.adcs_template + ] + end + } + ) + print_line(tbl.to_s) + end + + if mode == :delete + print_status("Deleted #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0 + end + end + + + # @param [String, nil] username Search for pkcs12 associated with this username + # @param [Array, nil] id_search List of pkcs12 IDs to search for + # @param [Workspace] workspace to search against + # @option [Symbol] :workspace The framework.db.workspace to search against (optional) + # @return [Array<>] + def pkcs12_search(username: nil, id_search: nil, workspace: framework.db.workspace) + pkcs12_results = [] + + if id_search.present? + begin + pkcs12_results += id_search.flat_map do |id| + pkcs12_storage.pkcs12( + workspace: workspace, + id: id + ) + end + rescue ActiveRecord::RecordNotFound => e + wlog("Record Not Found: #{e.message}") + print_warning("Not all records with the ids: #{id_search} could be found.") + print_warning('Please ensure all ids specified are available.') + end + elsif username.present? + realm = nil + if username.include?('@') + username, realm = username.split('@', 2) + end + pkcs12_results += pkcs12_storage.pkcs12( + workspace: workspace, + username: username, + realm: realm + ) + else + pkcs12_results += pkcs12_storage.pkcs12( + workspace: workspace + ) + end + + pkcs12_results.sort_by do |pkcs12| + [pkcs12.realm, pkcs12.username] + end + end + + # @return [Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadWrite] + def pkcs12_storage + @pkcs12_storage ||= Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework) + end + +end diff --git a/modules/auxiliary/scanner/ldap/ldap_login.rb b/modules/auxiliary/scanner/ldap/ldap_login.rb index 7c5efbabf439..e73aa03d6c14 100644 --- a/modules/auxiliary/scanner/ldap/ldap_login.rb +++ b/modules/auxiliary/scanner/ldap/ldap_login.rb @@ -115,6 +115,14 @@ def run_host(ip) realm_key = nil if opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::KERBEROS realm_key = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN + if datastore['CreateSession'] + # If kerberos auth is used and session creation is requested, we want to be able to read the cached tickets + # TODO: once the password issue (https://github.com/rapid7/metasploit-framework/issues/19743) is fixed + # we might prefer to check if the `password` option is set instead of `CreateSession`. If the user + # sets the password with Kerberos auth, it means he wants to test if it's valid, so we should not reuse + # a ticket from the cache. + opts[:kerberos_ticket_storage] = kerberos_ticket_storage({ read: true, write: true }) + end end scanner = Metasploit::Framework::LoginScanner::LDAP.new( diff --git a/modules/auxiliary/scanner/smb/smb_login.rb b/modules/auxiliary/scanner/smb/smb_login.rb index 0519b29657f9..f8eb49416b72 100644 --- a/modules/auxiliary/scanner/smb/smb_login.rb +++ b/modules/auxiliary/scanner/smb/smb_login.rb @@ -116,6 +116,17 @@ def run_host(ip) fail_with(Msf::Exploit::Failure::BadConfig, 'The SMBDomain option is required when using Kerberos authentication.') if datastore['SMBDomain'].blank? fail_with(Msf::Exploit::Failure::BadConfig, 'The DomainControllerRhost is required when using Kerberos authentication.') if datastore['DomainControllerRhost'].blank? + if datastore['CreateSession'] + # If kerberos auth is used and session creation is requested, we want to be able to read the cached tickets + # TODO: once the password issue (https://github.com/rapid7/metasploit-framework/issues/19743) is fixed + # we might prefer to check if the `password` option is set instead of `CreateSession`. If the user + # sets the password with Kerberos auth, it means he wants to test if it's valid, so we should not reuse + # a ticket from the cache. + krb_ticket_storage = kerberos_ticket_storage({ read: true, write: true }) + else + # Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module + krb_ticket_storage = kerberos_ticket_storage({ read: false, write: true }) + end kerberos_authenticator_factory = lambda do |username, password, realm| Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::SMB.new( host: datastore['DomainControllerRhost'], @@ -128,7 +139,7 @@ def run_host(ip) framework_module: self, cache_file: datastore['Smb::Krb5Ccname'].blank? ? nil : datastore['Smb::Krb5Ccname'], # Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module - ticket_storage: kerberos_ticket_storage({ read: false, write: true }) + ticket_storage: krb_ticket_storage ) end end