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
BUNDLED WITH
2.5.10
2.5.22
@@ -158,7 +158,7 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
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 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
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 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
)
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<String>] 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 }
@@ -41,7 +41,7 @@ module Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
[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
+1 -1
View File
@@ -40,7 +40,7 @@ module Msf
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 ]])
]
+12 -3
View File
@@ -20,11 +20,20 @@ module Msf::Exploit::Remote::Pkcs12
# @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})"
+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
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 @@ class Creds
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 @@ class Creds
jtr_val,
cracked_password_val
]
tbl << row
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('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 @@ class MetasploitModule < Msf::Auxiliary
],
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'
@@ -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_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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
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
@@ -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