Skip to content

Commit 5c3269e

Browse files
committed
Add database ref opts for kerberos and pkcs12
1 parent bebb43f commit 5c3269e

File tree

14 files changed

+192
-60
lines changed

14 files changed

+192
-60
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,4 +683,4 @@ DEPENDENCIES
683683
yard
684684

685685
BUNDLED WITH
686-
2.5.10
686+
2.5.22

lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def initialize(
158158
if cache_file.present?
159159
# the cache file is only used for loading credentials, it is *not* written to
160160
load_sname_hostname_credential_result = load_credential_from_file(cache_file, sname: nil, sname_hostname: @hostname)
161-
credential = load_sname_hostname_credential_result[:credential]
161+
credential = load_sname_hostname_credential_result&.fetch(:credential, nil)
162162
serviceclass = build_spn&.name_string&.first
163163
if credential && credential.server.components[0] != serviceclass
164164
old_sname = credential.server.components.snapshot.join('/')
@@ -170,12 +170,14 @@ def initialize(
170170
credential.ticket = ticket.encode
171171
elsif credential.nil? && hostname.present?
172172
load_sname_krbtgt_hostname_credential_result = load_credential_from_file(cache_file, sname: "krbtgt/#{hostname.split('.', 2).last}")
173-
credential = load_sname_krbtgt_hostname_credential_result[:credential]
173+
credential = load_sname_krbtgt_hostname_credential_result&.fetch(:credential, nil)
174174
end
175175
if credential.nil?
176176
print_error("Failed to load a usable credential from ticket file: #{cache_file}")
177-
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(', ')}:")
178-
print_error(load_sname_hostname_credential_result[:filter_reasons].join("\n").indent(2))
177+
if load_sname_hostname_credential_result
178+
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(', ')}:")
179+
print_error(load_sname_hostname_credential_result[:filter_reasons].join("\n").indent(2))
180+
end
179181

180182
if load_sname_krbtgt_hostname_credential_result
181183
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 = {})
10651067
)
10661068
end
10671069

1068-
# Load a credential object from a file for authentication. Credentials in the file will be filtered by multiple
1070+
# Load a credential object from a file or database entry for authentication. Credentials in the credential cache will be filtered by multiple
10691071
# attributes including their timestamps to ensure that the returned credential appears usable.
10701072
#
10711073
# @param [String] path The path to load a credential object from
10721074
# @return [Hash] :credential [Rex::Proto::Kerberos::CredentialCache::Krb5CacheCredential] the credential object for authentication
10731075
# @return [Hash] :filter_reasons [Array<String>] the reasons for filtering tickets
10741076
def load_credential_from_file(path, options = {})
1075-
unless File.readable?(path.to_s)
1076-
return nil
1077-
end
1077+
# Load a database reference or a path
1078+
if path&.start_with?('id:')
1079+
id = path.delete_prefix('id:')
1080+
storage = Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadOnly.new(framework: framework)
1081+
cache = storage.tickets({ id: id }).first&.ccache
1082+
unless cache
1083+
wlog("Invalid cache id #{id} provided")
1084+
return { credential: nil }
1085+
end
1086+
else
1087+
unless File.readable?(path.to_s)
1088+
wlog("Failed to load ticket file '#{path}' (file not readable)")
1089+
return nil
1090+
end
10781091

1079-
begin
1080-
cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path))
1081-
rescue StandardError => e
1082-
elog("Failed to load ticket file '#{path}' (parsing failed)", error: e)
1083-
return nil
1092+
begin
1093+
cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path))
1094+
rescue StandardError => e
1095+
elog("Failed to load ticket file '#{path}' (parsing failed)", error: e)
1096+
return nil
1097+
end
10841098
end
10851099

10861100
sname = options.fetch(:sname) { build_spn&.to_s }

lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def kerberos_auth_options(protocol:, auth_methods:)
4141
[false, 'The resolvable rhost for the Domain Controller'],
4242
conditions: option_conditions
4343
),
44-
Msf::OptPath.new(
44+
Msf::OptKerberosCredentialCache.new(
4545
"#{protocol}::Krb5Ccname",
4646
[false, 'The ccache file to use for kerberos authentication', nil],
4747
conditions: option_conditions

lib/msf/core/exploit/remote/ldap.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def initialize(info = {})
4040
Opt::Proxies,
4141
*kerberos_storage_options(protocol: 'LDAP'),
4242
*kerberos_auth_options(protocol: 'LDAP', auth_methods: Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS),
43-
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]),
43+
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]),
4444
OptFloat.new('LDAP::ConnectTimeout', [true, 'Timeout for LDAP connect', 10.0]),
4545
OptEnum.new('LDAP::Signing', [true, 'Use signed and sealed (encrypted) LDAP', 'auto', %w[ disabled auto required ]])
4646
]

lib/msf/core/exploit/remote/pkcs12/storage.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,20 @@ def initialize(framework: nil, framework_module: nil)
2020
# @param [String] cert_pass The certificate password
2121
# @param [String] workspace The workspace to restrict searches to
2222
def read_pkcs12_cert_path(cert_path, cert_pass = '', workspace: nil)
23-
is_readable = ::File.file?(cert_path) && ::File.readable?(cert_path)
24-
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.' unless is_readable
25-
data = File.binread(cert_path)
23+
if cert_path&.start_with?('id:')
24+
core = framework.db.creds({ workspace: workspace, id: cert_path.delete_prefix('id:') }).first
25+
raise Msf::ValidationError, 'Invalid cert id provided' unless core
26+
raise Msf::ValidationError, 'Invalid cert id provided - not a pkcs12 credential' unless core.private.type == 'Metasploit::Credential::Pkcs12'
27+
28+
data = Base64.decode64(core.private.data)
29+
else
30+
is_readable = ::File.file?(cert_path) && ::File.readable?(cert_path)
31+
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.' unless is_readable
32+
data = File.binread(cert_path)
33+
end
2634

2735
begin
36+
# TODO: Is it possible to read the cert pass from the db?
2837
pkcs12 = OpenSSL::PKCS12.new(data, cert_pass)
2938
rescue StandardError => e
3039
raise Msf::ValidationError, "Failed to load the PFX file (#{e})"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf
4+
###
5+
#
6+
# Opt that can be reference a database Id or a file on disk; Valid examples:
7+
# - /tmp/foo.txt
8+
# - id:123
9+
###
10+
class OptDatabaseRefOrPath < OptBase
11+
def normalize(value)
12+
return value if value.nil? || value.to_s.empty? || value.start_with?('id:')
13+
14+
File.expand_path(value)
15+
end
16+
17+
def validate_on_assignment?
18+
false
19+
end
20+
21+
# Generally, 'value' should be a file that exists, or an integer database id.
22+
def valid?(value, check_empty: true, datastore: nil)
23+
return false if check_empty && empty_required_value?(value)
24+
25+
if value && !value.empty?
26+
if value.start_with?('id:')
27+
return value.match?(/^id:\d+$/)
28+
end
29+
30+
unless File.exist?(File.expand_path(value))
31+
return false
32+
end
33+
end
34+
super
35+
end
36+
end
37+
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf
4+
###
5+
#
6+
# Pkcs12 cert that can either exist on disk, or as a database core ID
7+
#
8+
###
9+
class OptKerberosCredentialCache < OptDatabaseRefOrPath
10+
def type
11+
'kerberos_credential_cache'
12+
end
13+
end
14+
end

lib/msf/core/opt_pkcs12_cert.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf
4+
###
5+
#
6+
# Pkcs12 cert that can either exist on disk, or as a database core ID
7+
#
8+
###
9+
class OptPkcs12Cert < OptDatabaseRefOrPath
10+
def type
11+
'pkcs12_cert'
12+
end
13+
end
14+
end

lib/msf/ui/console/command_dispatcher/creds.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def creds_search(*args)
344344
set_rhosts = false
345345
truncate = true
346346

347-
cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ]
347+
cred_table_columns = [ 'id', 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ]
348348
delete_count = 0
349349
search_term = nil
350350

@@ -506,7 +506,8 @@ def creds_search(*args)
506506
service_info = build_service_info(service)
507507
end
508508
cracked_password_val = cracked_password_core&.private&.data.to_s
509-
tbl << [
509+
row = [
510+
core.id,
510511
host,
511512
origin,
512513
service_info,
@@ -517,6 +518,7 @@ def creds_search(*args)
517518
jtr_val,
518519
cracked_password_val
519520
]
521+
tbl << row
520522
end
521523
end
522524

modules/auxiliary/admin/kerberos/get_ticket.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def initialize(info = {})
4848
OptString.new('DOMAIN', [ false, 'The Fully Qualified Domain Name (FQDN). Ex: mydomain.local' ]),
4949
OptString.new('USERNAME', [ false, 'The domain user' ]),
5050
OptString.new('PASSWORD', [ false, 'The domain user\'s password' ]),
51-
OptPath.new('CERT_FILE', [ false, 'The PKCS12 (.pfx) certificate file to authenticate with' ]),
51+
OptPkcs12Cert.new('CERT_FILE', [ false, 'The PKCS12 (.pfx) certificate file to authenticate with' ]),
5252
OptString.new('CERT_PASSWORD', [ false, 'The certificate file\'s password' ]),
5353
OptString.new(
5454
'NTHASH', [
@@ -76,7 +76,7 @@ def initialize(info = {})
7676
],
7777
conditions: %w[ACTION == GET_TGS]
7878
),
79-
OptPath.new(
79+
OptKerberosCredentialCache.new(
8080
'Krb5Ccname', [
8181
false,
8282
'The Kerberos TGT to use when requesting the service ticket. If unset, the database will be checked'

0 commit comments

Comments
 (0)