Skip to content

Add database ref opts for kerberos and pkcs12 #20457

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -683,4 +683,4 @@ DEPENDENCIES
yard

BUNDLED WITH
2.5.10
2.5.22
40 changes: 27 additions & 13 deletions lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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('/')
Expand All @@ -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(', ')}")
Expand Down Expand Up @@ -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<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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/msf/core/exploit/remote/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]])
]
Expand Down
15 changes: 12 additions & 3 deletions lib/msf/core/exploit/remote/pkcs12/storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
Expand Down
37 changes: 37 additions & 0 deletions lib/msf/core/opt_database_ref_or_path.rb
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions lib/msf/core/opt_kerberos_credential_cache.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions lib/msf/core/opt_pkcs12_cert.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 4 additions & 2 deletions lib/msf/ui/console/command_dispatcher/creds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -517,6 +518,7 @@ def creds_search(*args)
jtr_val,
cracked_password_val
]
tbl << row
end
end

Expand Down
4 changes: 2 additions & 2 deletions modules/auxiliary/admin/kerberos/get_ticket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand Down Expand Up @@ -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'
Expand Down
7 changes: 7 additions & 0 deletions spec/lib/msf/core/opt_kerberos_credential_cache_spec.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions spec/lib/msf/core/opt_pkcs12_cert_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading