Add database ref opts for kerberos and pkcs12

This commit is contained in:
adfoster-r7
2025-08-11 11:41:05 +01:00
parent be3d77715e
commit d13dc197b7
14 changed files with 192 additions and 60 deletions
+1 -1
View File
@@ -683,4 +683,4 @@ DEPENDENCIES
yard yard
BUNDLED WITH BUNDLED WITH
2.5.10 2.5.22
@@ -158,7 +158,7 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
if cache_file.present? if cache_file.present?
# the cache file is only used for loading credentials, it is *not* written to # 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) 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 serviceclass = build_spn&.name_string&.first
if credential && credential.server.components[0] != serviceclass if credential && credential.server.components[0] != serviceclass
old_sname = credential.server.components.snapshot.join('/') old_sname = credential.server.components.snapshot.join('/')
@@ -170,12 +170,14 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
credential.ticket = ticket.encode credential.ticket = ticket.encode
elsif credential.nil? && hostname.present? elsif credential.nil? && hostname.present?
load_sname_krbtgt_hostname_credential_result = load_credential_from_file(cache_file, sname: "krbtgt/#{hostname.split('.', 2).last}") 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 end
if credential.nil? if credential.nil?
print_error("Failed to load a usable credential from ticket file: #{cache_file}") 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(', ')}:") if load_sname_hostname_credential_result
print_error(load_sname_hostname_credential_result[:filter_reasons].join("\n").indent(2)) 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 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(', ')}") 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 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
) )
end 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. # attributes including their timestamps to ensure that the returned credential appears usable.
# #
# @param [String] path The path to load a credential object from # @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] :credential [Rex::Proto::Kerberos::CredentialCache::Krb5CacheCredential] the credential object for authentication
# @return [Hash] :filter_reasons [Array<String>] the reasons for filtering tickets # @return [Hash] :filter_reasons [Array<String>] the reasons for filtering tickets
def load_credential_from_file(path, options = {}) def load_credential_from_file(path, options = {})
unless File.readable?(path.to_s) # Load a database reference or a path
return nil if path&.start_with?('id:')
end 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 begin
cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path)) cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path))
rescue StandardError => e rescue StandardError => e
elog("Failed to load ticket file '#{path}' (parsing failed)", error: e) elog("Failed to load ticket file '#{path}' (parsing failed)", error: e)
return nil return nil
end
end end
sname = options.fetch(:sname) { build_spn&.to_s } sname = options.fetch(:sname) { build_spn&.to_s }
@@ -41,7 +41,7 @@ module Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
[false, 'The resolvable rhost for the Domain Controller'], [false, 'The resolvable rhost for the Domain Controller'],
conditions: option_conditions conditions: option_conditions
), ),
Msf::OptPath.new( Msf::OptKerberosCredentialCache.new(
"#{protocol}::Krb5Ccname", "#{protocol}::Krb5Ccname",
[false, 'The ccache file to use for kerberos authentication', nil], [false, 'The ccache file to use for kerberos authentication', nil],
conditions: option_conditions conditions: option_conditions
+1 -1
View File
@@ -40,7 +40,7 @@ module Msf
Opt::Proxies, Opt::Proxies,
*kerberos_storage_options(protocol: 'LDAP'), *kerberos_storage_options(protocol: 'LDAP'),
*kerberos_auth_options(protocol: 'LDAP', auth_methods: Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS), *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]), 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 ]]) OptEnum.new('LDAP::Signing', [true, 'Use signed and sealed (encrypted) LDAP', 'auto', %w[ disabled auto required ]])
] ]
+12 -3
View File
@@ -20,11 +20,20 @@ module Msf::Exploit::Remote::Pkcs12
# @param [String] cert_pass The certificate password # @param [String] cert_pass The certificate password
# @param [String] workspace The workspace to restrict searches to # @param [String] workspace The workspace to restrict searches to
def read_pkcs12_cert_path(cert_path, cert_pass = '', workspace: nil) def read_pkcs12_cert_path(cert_path, cert_pass = '', workspace: nil)
is_readable = ::File.file?(cert_path) && ::File.readable?(cert_path) if cert_path&.start_with?('id:')
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.' unless is_readable core = framework.db.creds({ workspace: workspace, id: cert_path.delete_prefix('id:') }).first
data = File.binread(cert_path) 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 begin
# TODO: Is it possible to read the cert pass from the db?
pkcs12 = OpenSSL::PKCS12.new(data, cert_pass) pkcs12 = OpenSSL::PKCS12.new(data, cert_pass)
rescue StandardError => e rescue StandardError => e
raise Msf::ValidationError, "Failed to load the PFX file (#{e})" raise Msf::ValidationError, "Failed to load the PFX file (#{e})"
+37
View File
@@ -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
@@ -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
+14
View File
@@ -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
@@ -344,7 +344,7 @@ class Creds
set_rhosts = false set_rhosts = false
truncate = true 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 delete_count = 0
search_term = nil search_term = nil
@@ -506,7 +506,8 @@ class Creds
service_info = build_service_info(service) service_info = build_service_info(service)
end end
cracked_password_val = cracked_password_core&.private&.data.to_s cracked_password_val = cracked_password_core&.private&.data.to_s
tbl << [ row = [
core.id,
host, host,
origin, origin,
service_info, service_info,
@@ -517,6 +518,7 @@ class Creds
jtr_val, jtr_val,
cracked_password_val cracked_password_val
] ]
tbl << row
end end
end end
@@ -48,7 +48,7 @@ class MetasploitModule < Msf::Auxiliary
OptString.new('DOMAIN', [ false, 'The Fully Qualified Domain Name (FQDN). Ex: mydomain.local' ]), OptString.new('DOMAIN', [ false, 'The Fully Qualified Domain Name (FQDN). Ex: mydomain.local' ]),
OptString.new('USERNAME', [ false, 'The domain user' ]), OptString.new('USERNAME', [ false, 'The domain user' ]),
OptString.new('PASSWORD', [ false, 'The domain user\'s password' ]), 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('CERT_PASSWORD', [ false, 'The certificate file\'s password' ]),
OptString.new( OptString.new(
'NTHASH', [ 'NTHASH', [
@@ -76,7 +76,7 @@ class MetasploitModule < Msf::Auxiliary
], ],
conditions: %w[ACTION == GET_TGS] conditions: %w[ACTION == GET_TGS]
), ),
OptPath.new( OptKerberosCredentialCache.new(
'Krb5Ccname', [ 'Krb5Ccname', [
false, false,
'The Kerberos TGT to use when requesting the service ticket. If unset, the database will be checked' 'The Kerberos TGT to use when requesting the service ticket. If unset, the database will be checked'
@@ -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
@@ -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
@@ -18,6 +18,14 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
it { is_expected.to respond_to :creds_add } it { is_expected.to respond_to :creds_add }
it { is_expected.to respond_to :creds_search } 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 describe '#cmd_creds' do
let(:username) { 'thisuser' } let(:username) { 'thisuser' }
let(:password) { 'thispass' } let(:password) { 'thispass' }
@@ -70,9 +78,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
thisuser thispass Password id thisuser thispass Password
TABLE TABLE
end end
@@ -83,8 +91,8 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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 TABLE
end end
@@ -96,9 +104,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
nonblank_pass Password id nonblank_pass Password
TABLE TABLE
end end
@@ -110,9 +118,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
nonblank_user Password id nonblank_user Password
TABLE TABLE
end end
@@ -127,8 +135,8 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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 TABLE
end end
@@ -140,8 +148,8 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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 TABLE
end end
@@ -166,9 +174,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
this_username some_hash Nonreplayable hash this_cracked_password id this_username some_hash Nonreplayable hash this_cracked_password
TABLE TABLE
end end
it "should show the user given passwords on private column instead of cracked_password column" do it "should show the user given passwords on private column instead of cracked_password column" do
@@ -177,9 +185,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
thisuser thispass Password id thisuser thispass Password
TABLE TABLE
end end
@@ -263,9 +271,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
thisuser thispass Password id thisuser thispass Password
TABLE TABLE
end end
@@ -288,10 +296,10 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
thisuser thispass Password id thisuser thispass Password
this_username this_cracked_password Password id this_username this_cracked_password Password
TABLE TABLE
end end
@@ -305,9 +313,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
thisuser 1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c NTLM hash id thisuser 1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c NTLM hash
TABLE TABLE
end end
@@ -321,9 +329,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
thisuser asdf Nonreplayable hash id thisuser asdf Nonreplayable hash
TABLE TABLE
end end
@@ -338,9 +346,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
Credentials 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
---- ------ ------- ------ ------- ----- ------------ ---------- ---------------- -- ---- ------ ------- ------ ------- ----- ------------ ---------- ----------------
#{private_str} Pkcs12 (pfx) id #{private_str} Pkcs12 (pfx)
TABLE TABLE
end end
@@ -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