diff --git a/Gemfile.lock b/Gemfile.lock index 03d6e65f32044..a5bfa0ac8fa56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -683,4 +683,4 @@ DEPENDENCIES yard BUNDLED WITH - 2.5.10 + 2.5.22 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 53d6edbf4c70d..2201f84db3478 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -158,7 +158,7 @@ def initialize( if cache_file.present? # the cache file is only used for loading credentials, it is *not* written to load_sname_hostname_credential_result = load_credential_from_file(cache_file, sname: nil, sname_hostname: @hostname) - credential = load_sname_hostname_credential_result[:credential] + credential = load_sname_hostname_credential_result&.fetch(:credential, nil) serviceclass = build_spn&.name_string&.first if credential && credential.server.components[0] != serviceclass old_sname = credential.server.components.snapshot.join('/') @@ -170,12 +170,14 @@ def initialize( credential.ticket = ticket.encode elsif credential.nil? && hostname.present? load_sname_krbtgt_hostname_credential_result = load_credential_from_file(cache_file, sname: "krbtgt/#{hostname.split('.', 2).last}") - credential = load_sname_krbtgt_hostname_credential_result[:credential] + credential = load_sname_krbtgt_hostname_credential_result&.fetch(:credential, nil) end if credential.nil? print_error("Failed to load a usable credential from ticket file: #{cache_file}") - print_error("Attempt failed to find a valid credential in #{cache_file} for #{load_sname_hostname_credential_result[:filter].map { |k, v| "#{k}=#{v.inspect}" }.join(', ')}:") - print_error(load_sname_hostname_credential_result[:filter_reasons].join("\n").indent(2)) + if load_sname_hostname_credential_result + print_error("Attempt failed to find a valid credential in #{cache_file} for #{load_sname_hostname_credential_result[:filter].map { |k, v| "#{k}=#{v.inspect}" }.join(', ')}:") + print_error(load_sname_hostname_credential_result[:filter_reasons].join("\n").indent(2)) + end if load_sname_krbtgt_hostname_credential_result print_error("Attempt failed to find a valid credential in #{cache_file} for #{load_sname_krbtgt_hostname_credential_result[:filter].map { |k, v| "#{k}=#{v.inspect}" }.join(', ')}") @@ -1065,22 +1067,34 @@ def get_cached_credential(options = {}) ) end - # Load a credential object from a file for authentication. Credentials in the file will be filtered by multiple + # Load a credential object from a file or database entry for authentication. Credentials in the credential cache will be filtered by multiple # attributes including their timestamps to ensure that the returned credential appears usable. # # @param [String] path The path to load a credential object from # @return [Hash] :credential [Rex::Proto::Kerberos::CredentialCache::Krb5CacheCredential] the credential object for authentication # @return [Hash] :filter_reasons [Array] the reasons for filtering tickets def load_credential_from_file(path, options = {}) - unless File.readable?(path.to_s) - return nil - end + # Load a database reference or a path + if path&.start_with?('id:') + id = path.delete_prefix('id:') + storage = Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadOnly.new(framework: framework) + cache = storage.tickets({ id: id }).first&.ccache + unless cache + wlog("Invalid cache id #{id} provided") + return { credential: nil } + end + else + unless File.readable?(path.to_s) + wlog("Failed to load ticket file '#{path}' (file not readable)") + return nil + end - begin - cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path)) - rescue StandardError => e - elog("Failed to load ticket file '#{path}' (parsing failed)", error: e) - return nil + begin + cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path)) + rescue StandardError => e + elog("Failed to load ticket file '#{path}' (parsing failed)", error: e) + return nil + end end sname = options.fetch(:sname) { build_spn&.to_s } diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb index c946627bc1c96..a52253b7640ad 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb @@ -41,7 +41,7 @@ def kerberos_auth_options(protocol:, auth_methods:) [false, 'The resolvable rhost for the Domain Controller'], conditions: option_conditions ), - Msf::OptPath.new( + Msf::OptKerberosCredentialCache.new( "#{protocol}::Krb5Ccname", [false, 'The ccache file to use for kerberos authentication', nil], conditions: option_conditions diff --git a/lib/msf/core/exploit/remote/ldap.rb b/lib/msf/core/exploit/remote/ldap.rb index 29712d376024f..e7d15097473b7 100644 --- a/lib/msf/core/exploit/remote/ldap.rb +++ b/lib/msf/core/exploit/remote/ldap.rb @@ -40,7 +40,7 @@ def initialize(info = {}) Opt::Proxies, *kerberos_storage_options(protocol: 'LDAP'), *kerberos_auth_options(protocol: 'LDAP', auth_methods: Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS), - Msf::OptPath.new('LDAP::CertFile', [false, 'The path to the PKCS12 (.pfx) certificate file to authenticate with'], conditions: ['LDAP::Auth', '==', Msf::Exploit::Remote::AuthOption::SCHANNEL]), + Msf::OptPkcs12Cert.new('LDAP::CertFile', [false, 'The path to the PKCS12 (.pfx) certificate file to authenticate with'], conditions: ['LDAP::Auth', '==', Msf::Exploit::Remote::AuthOption::SCHANNEL]), OptFloat.new('LDAP::ConnectTimeout', [true, 'Timeout for LDAP connect', 10.0]), OptEnum.new('LDAP::Signing', [true, 'Use signed and sealed (encrypted) LDAP', 'auto', %w[ disabled auto required ]]) ] diff --git a/lib/msf/core/exploit/remote/pkcs12/storage.rb b/lib/msf/core/exploit/remote/pkcs12/storage.rb index 8e23b660ab621..5c95170eefaff 100644 --- a/lib/msf/core/exploit/remote/pkcs12/storage.rb +++ b/lib/msf/core/exploit/remote/pkcs12/storage.rb @@ -20,11 +20,20 @@ def initialize(framework: nil, framework_module: nil) # @param [String] cert_pass The certificate password # @param [String] workspace The workspace to restrict searches to def read_pkcs12_cert_path(cert_path, cert_pass = '', workspace: nil) - is_readable = ::File.file?(cert_path) && ::File.readable?(cert_path) - raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.' unless is_readable - data = File.binread(cert_path) + if cert_path&.start_with?('id:') + core = framework.db.creds({ workspace: workspace, id: cert_path.delete_prefix('id:') }).first + raise Msf::ValidationError, 'Invalid cert id provided' unless core + raise Msf::ValidationError, 'Invalid cert id provided - not a pkcs12 credential' unless core.private.type == 'Metasploit::Credential::Pkcs12' + + data = Base64.decode64(core.private.data) + else + is_readable = ::File.file?(cert_path) && ::File.readable?(cert_path) + raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.' unless is_readable + data = File.binread(cert_path) + end begin + # TODO: Is it possible to read the cert pass from the db? pkcs12 = OpenSSL::PKCS12.new(data, cert_pass) rescue StandardError => e raise Msf::ValidationError, "Failed to load the PFX file (#{e})" diff --git a/lib/msf/core/opt_database_ref_or_path.rb b/lib/msf/core/opt_database_ref_or_path.rb new file mode 100644 index 0000000000000..1b48a16cf9dff --- /dev/null +++ b/lib/msf/core/opt_database_ref_or_path.rb @@ -0,0 +1,37 @@ +# -*- coding: binary -*- + +module Msf + ### + # + # Opt that can be reference a database Id or a file on disk; Valid examples: + # - /tmp/foo.txt + # - id:123 + ### + class OptDatabaseRefOrPath < OptBase + def normalize(value) + return value if value.nil? || value.to_s.empty? || value.start_with?('id:') + + File.expand_path(value) + end + + def validate_on_assignment? + false + end + + # Generally, 'value' should be a file that exists, or an integer database id. + def valid?(value, check_empty: true, datastore: nil) + return false if check_empty && empty_required_value?(value) + + if value && !value.empty? + if value.start_with?('id:') + return value.match?(/^id:\d+$/) + end + + unless File.exist?(File.expand_path(value)) + return false + end + end + super + end + end +end diff --git a/lib/msf/core/opt_kerberos_credential_cache.rb b/lib/msf/core/opt_kerberos_credential_cache.rb new file mode 100644 index 0000000000000..dbf41da5f26a9 --- /dev/null +++ b/lib/msf/core/opt_kerberos_credential_cache.rb @@ -0,0 +1,14 @@ +# -*- coding: binary -*- + +module Msf + ### + # + # Pkcs12 cert that can either exist on disk, or as a database core ID + # + ### + class OptKerberosCredentialCache < OptDatabaseRefOrPath + def type + 'kerberos_credential_cache' + end + end +end diff --git a/lib/msf/core/opt_pkcs12_cert.rb b/lib/msf/core/opt_pkcs12_cert.rb new file mode 100644 index 0000000000000..1ad2b0dd8a018 --- /dev/null +++ b/lib/msf/core/opt_pkcs12_cert.rb @@ -0,0 +1,14 @@ +# -*- coding: binary -*- + +module Msf + ### + # + # Pkcs12 cert that can either exist on disk, or as a database core ID + # + ### + class OptPkcs12Cert < OptDatabaseRefOrPath + def type + 'pkcs12_cert' + end + end +end diff --git a/lib/msf/ui/console/command_dispatcher/creds.rb b/lib/msf/ui/console/command_dispatcher/creds.rb index f4bddd358cbcd..7228275b99606 100644 --- a/lib/msf/ui/console/command_dispatcher/creds.rb +++ b/lib/msf/ui/console/command_dispatcher/creds.rb @@ -344,7 +344,7 @@ def creds_search(*args) set_rhosts = false truncate = true - cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ] + cred_table_columns = [ 'id', 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ] delete_count = 0 search_term = nil @@ -506,7 +506,8 @@ def creds_search(*args) service_info = build_service_info(service) end cracked_password_val = cracked_password_core&.private&.data.to_s - tbl << [ + row = [ + core.id, host, origin, service_info, @@ -517,6 +518,7 @@ def creds_search(*args) jtr_val, cracked_password_val ] + tbl << row end end diff --git a/modules/auxiliary/admin/kerberos/get_ticket.rb b/modules/auxiliary/admin/kerberos/get_ticket.rb index 009ebb67d2d9a..5841f0db61307 100644 --- a/modules/auxiliary/admin/kerberos/get_ticket.rb +++ b/modules/auxiliary/admin/kerberos/get_ticket.rb @@ -48,7 +48,7 @@ def initialize(info = {}) OptString.new('DOMAIN', [ false, 'The Fully Qualified Domain Name (FQDN). Ex: mydomain.local' ]), OptString.new('USERNAME', [ false, 'The domain user' ]), OptString.new('PASSWORD', [ false, 'The domain user\'s password' ]), - OptPath.new('CERT_FILE', [ false, 'The PKCS12 (.pfx) certificate file to authenticate with' ]), + OptPkcs12Cert.new('CERT_FILE', [ false, 'The PKCS12 (.pfx) certificate file to authenticate with' ]), OptString.new('CERT_PASSWORD', [ false, 'The certificate file\'s password' ]), OptString.new( 'NTHASH', [ @@ -76,7 +76,7 @@ def initialize(info = {}) ], conditions: %w[ACTION == GET_TGS] ), - OptPath.new( + OptKerberosCredentialCache.new( 'Krb5Ccname', [ false, 'The Kerberos TGT to use when requesting the service ticket. If unset, the database will be checked' diff --git a/spec/lib/msf/core/opt_kerberos_credential_cache_spec.rb b/spec/lib/msf/core/opt_kerberos_credential_cache_spec.rb new file mode 100644 index 0000000000000..eb693a239ff0b --- /dev/null +++ b/spec/lib/msf/core/opt_kerberos_credential_cache_spec.rb @@ -0,0 +1,7 @@ +# -*- coding:binary -*- + +require 'spec_helper' + +RSpec.describe Msf::OptKerberosCredentialCache do + it_behaves_like 'a database ref or path option', expected_type: 'kerberos_credential_cache' +end diff --git a/spec/lib/msf/core/opt_pkcs12_cert_spec.rb b/spec/lib/msf/core/opt_pkcs12_cert_spec.rb new file mode 100644 index 0000000000000..9092f47d4cc2f --- /dev/null +++ b/spec/lib/msf/core/opt_pkcs12_cert_spec.rb @@ -0,0 +1,7 @@ +# -*- coding:binary -*- + +require 'spec_helper' + +RSpec.describe Msf::OptPkcs12Cert do + it_behaves_like 'a database ref or path option', expected_type: 'pkcs12_cert' +end 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 9ffdbf37a69f5..1856c08899fa2 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb @@ -18,6 +18,14 @@ it { is_expected.to respond_to :creds_add } it { is_expected.to respond_to :creds_search } + before(:each) do + # Replace the incremental database ID to ensure deterministic tests + allow_any_instance_of(Rex::Text::WrappedTable).to receive(:<<).and_wrap_original do |original, row| + row_without_id = ['id'] + row.dup[1..] + original.call row_without_id + end + end + describe '#cmd_creds' do let(:username) { 'thisuser' } let(:password) { 'thispass' } @@ -70,9 +78,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - thisuser thispass Password + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id thisuser thispass Password TABLE end @@ -83,8 +91,8 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- TABLE end @@ -96,9 +104,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - nonblank_pass Password + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id nonblank_pass Password TABLE end @@ -110,9 +118,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - nonblank_user Password + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id nonblank_user Password TABLE end @@ -127,8 +135,8 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- TABLE end @@ -140,8 +148,8 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- TABLE end @@ -166,9 +174,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - this_username some_hash Nonreplayable hash this_cracked_password + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id this_username some_hash Nonreplayable hash this_cracked_password TABLE end it "should show the user given passwords on private column instead of cracked_password column" do @@ -177,9 +185,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - thisuser thispass Password + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id thisuser thispass Password TABLE end @@ -263,9 +271,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - thisuser thispass Password + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id thisuser thispass Password TABLE end @@ -288,10 +296,10 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - thisuser thispass Password - this_username this_cracked_password Password + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id thisuser thispass Password + id this_username this_cracked_password Password TABLE end @@ -305,9 +313,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - thisuser 1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c NTLM hash + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id thisuser 1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c NTLM hash TABLE end @@ -321,9 +329,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - thisuser asdf Nonreplayable hash + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id thisuser asdf Nonreplayable hash TABLE end @@ -338,9 +346,9 @@ Credentials =========== - host origin service public private realm private_type JtR Format cracked_password - ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- - #{private_str} Pkcs12 (pfx) + id host origin service public private realm private_type JtR Format cracked_password + -- ---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- + id #{private_str} Pkcs12 (pfx) TABLE end diff --git a/spec/support/shared/examples/a_database_ref_or_path_option.rb b/spec/support/shared/examples/a_database_ref_or_path_option.rb new file mode 100644 index 0000000000000..8bad182e1ce74 --- /dev/null +++ b/spec/support/shared/examples/a_database_ref_or_path_option.rb @@ -0,0 +1,20 @@ +# -*- coding:binary -*- + +RSpec.shared_examples_for "a database ref or path option" do |options| + valid_values = [ + { value: __FILE__, normalized: __FILE__ }, + { value: '~', normalized: ::File.expand_path('~') }, + { value: 'id:1', normalized: 'id:1' }, + ] + invalid_values = [ + { value: '0.1' }, + { value: '-1' }, + { value: '65536' }, + { value: '$' }, + { value: 'id:-1' }, + { value: 'id:' }, + ] + + it_behaves_like "an option", valid_values, invalid_values, options.fetch(:expected_type) +end +