Skip to content

Commit 571e25d

Browse files
committed
Add database ref opts for kerberos and pkcs12
1 parent 2734dae commit 571e25d

File tree

17 files changed

+433
-42
lines changed

17 files changed

+433
-42
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/auxiliary/report_summary.rb

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,23 @@ def login_credentials(credential_data)
7171
def create_credential_login(credential_data)
7272
return super unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) && datastore['ShowSuccessfulLogins'] && @report
7373

74-
@report[rhost] = { successful_logins: [] }
74+
@report[rhost] ||= {}
75+
@report[rhost][:successful_logins] ||= []
7576
@report[rhost][:successful_logins] << login_credentials(credential_data)
7677
super
7778
end
7879

80+
def report_successful_login(public:, private:)
81+
return super unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) && datastore['ShowSuccessfulLogins'] && @report
82+
83+
@report[rhost] ||= {}
84+
@report[rhost][:successful_logins] ||= []
85+
@report[rhost][:successful_logins] << {
86+
public: public,
87+
private_data: private
88+
}
89+
end
90+
7991
# Creates a credential and adds to to the DB if one is present, then calls create_credential_login to
8092
# attempt a login
8193
#
@@ -90,7 +102,8 @@ def create_credential_login(credential_data)
90102
def create_credential_and_login(credential_data)
91103
return super unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) && datastore['ShowSuccessfulLogins'] && @report
92104

93-
@report[rhost] = { successful_logins: [] }
105+
@report[rhost] ||= {}
106+
@report[rhost][:successful_logins] ||= []
94107
@report[rhost][:successful_logins] << login_credentials(credential_data)
95108
super
96109
end
@@ -107,14 +120,9 @@ def create_credential_and_login(credential_data)
107120
def start_session(obj, info, ds_merge, crlf = false, sock = nil, sess = nil)
108121
return super unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) && datastore['ShowSuccessfulLogins']
109122

110-
unless @report && @report[rhost]
111-
elog("No RHOST found in report, skipping reporting for #{rhost}")
112-
print_brute level: :error, ip: rhost, msg: "No RHOST found in report, skipping reporting for #{rhost}"
113-
return super
114-
end
115-
116123
result = super
117-
@report[rhost].merge!({ successful_sessions: [] })
124+
@report[rhost] ||= {}
125+
@report[rhost][:successful_sessions] ||= []
118126
@report[rhost][:successful_sessions] << result
119127
result
120128
end

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

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
9090
:print_status,
9191
:print_good,
9292
:vprint_error,
93+
:print_error,
9394
:vprint_status,
9495
:workspace
9596

@@ -156,7 +157,8 @@ def initialize(
156157
credential = nil
157158
if cache_file.present?
158159
# the cache file is only used for loading credentials, it is *not* written to
159-
credential = load_credential_from_file(cache_file, sname: nil, sname_hostname: @hostname)
160+
load_sname_hostname_credential_result = load_credential_from_file(cache_file, sname: nil, sname_hostname: @hostname)
161+
credential = load_sname_hostname_credential_result[:credential]
160162
serviceclass = build_spn.name_string.first
161163
if credential && credential.server.components[0] != serviceclass
162164
old_sname = credential.server.components.snapshot.join('/')
@@ -167,9 +169,18 @@ def initialize(
167169
ticket.sname.name_string[0] = serviceclass
168170
credential.ticket = ticket.encode
169171
elsif credential.nil? && hostname.present?
170-
credential = load_credential_from_file(cache_file, sname: "krbtgt/#{hostname.split('.', 2).last}")
172+
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]
171174
end
172175
if credential.nil?
176+
vprint_error("Failed to load a usable credential from ticket file: #{cache_file}")
177+
vprint_error("Attempt: finding a valid credential in #{cache_file} for sname: nil, and sname_hostname: #{@hostname}:")
178+
vprint_error(load_sname_hostname_credential_result[:filter_reasons].join("\n").indent(2))
179+
180+
if load_sname_krbtgt_hostname_credential_result
181+
vprint_error("Attempt: Failed finding a valid credential in #{cache_file} for sname: #{"krbtgt/#{hostname.split('.', 2).last}"}")
182+
vprint_error(load_sname_krbtgt_hostname_credential_result[:filter_reasons].join("\n").indent(2))
183+
end
173184
raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new("Failed to load a usable credential from ticket file: #{cache_file}")
174185
end
175186
print_status("Loaded a credential from ticket file: #{cache_file}")
@@ -254,6 +265,7 @@ def authenticate(options = {})
254265
elsif options[:credential]
255266
auth_context = authenticate_via_krb5_ccache_credential_tgs(options[:credential], options)
256267
else
268+
# TODO: If krbcachemode=none is set, should this be getting used?
257269
pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework, framework_module: framework_module)
258270
pkcs12_results = pkcs12_storage.pkcs12(
259271
workspace: workspace,
@@ -361,7 +373,7 @@ def build_spn(options = {})
361373
# @return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] The ccache credential
362374
def request_tgt_only(options = {})
363375
if options[:cache_file]
364-
credential = load_credential_from_file(options[:cache_file])
376+
credential = load_credential_from_file(options[:cache_file])[:credential]
365377
else
366378
credential = get_cached_credential(
367379
options.merge(
@@ -1054,67 +1066,92 @@ def get_cached_credential(options = {})
10541066
)
10551067
end
10561068

1057-
# Load a credential object from a file for authentication. Credentials in the file will be filtered by multiple
1069+
# Load a credential object from a file or database entry for authentication. Credentials in the credential cache will be filtered by multiple
10581070
# attributes including their timestamps to ensure that the returned credential appears usable.
10591071
#
1060-
# @param [String] file_path The file path to load a credential object from
1072+
# @param [path] path The file path to load a credential object from or database string reference
10611073
# @return [Rex::Proto::Kerberos::CredentialCache::Krb5CacheCredential] the credential object for authentication
1062-
def load_credential_from_file(file_path, options = {})
1063-
unless File.readable?(file_path.to_s)
1064-
wlog("Failed to load ticket file '#{file_path}' (file not readable)")
1065-
return nil
1066-
end
1074+
def load_credential_from_file(path, options = {})
1075+
# Load a database reference or a path
1076+
if path.start_with?('id:')
1077+
id = path.delete_prefix('id:')
1078+
storage = Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadOnly.new(framework: framework)
1079+
cache = storage.tickets({ id: id }).first&.ccache
1080+
if !cache
1081+
wlog("Invalid cache id #{id} provided") unless cache
1082+
return { credential: nil }
1083+
end
1084+
else
1085+
unless File.readable?(file_path.to_s)
1086+
wlog("Failed to load ticket file '#{file_path}' (file not readable)")
1087+
return nil
1088+
end
10671089

1068-
begin
1069-
cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(file_path))
1070-
rescue StandardError => e
1071-
elog("Failed to load ticket file '#{file_path}' (parsing failed)", error: e)
1072-
return nil
1090+
begin
1091+
cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(file_path))
1092+
rescue StandardError => e
1093+
elog("Failed to load ticket file '#{file_path}' (parsing failed)", error: e)
1094+
return nil
1095+
end
10731096
end
10741097

10751098
sname = options.fetch(:sname) { build_spn&.to_s }
10761099
sname_hostname = options.fetch(:sname_hostname, nil)
10771100
now = Time.now.utc
10781101

1102+
filter_reasons = []
1103+
10791104
cache.credentials.to_ary.each.with_index(1) do |credential, index|
10801105
tkt_start = credential.starttime == Time.at(0).utc ? credential.authtime : credential.starttime
10811106
tkt_end = credential.endtime
10821107

10831108
unless tkt_start < now
1084-
wlog("Filtered credential #{file_path} ##{index} reason: Ticket start time is before now (start: #{tkt_start})")
1109+
filter_reason = "Filtered credential #{path} ##{index} reason: Ticket start time is before now (start: #{tkt_start})"
1110+
filter_reasons << filter_reason
1111+
wlog(filter_reason)
10851112
next
10861113
end
10871114

10881115
unless now < tkt_end
1089-
wlog("Filtered credential #{file_path} ##{index} reason: Ticket is expired (expiration: #{tkt_end})")
1116+
wlog("Filtered credential #{path} ##{index} reason: Ticket is expired (expiration: #{tkt_end})")
1117+
filter_reasons << filter_reason
1118+
wlog(filter_reason)
10901119
next
10911120
end
10921121

10931122
unless !@realm || @realm.casecmp?(credential.server.realm.to_s)
1094-
wlog("Filtered credential #{file_path} ##{index} reason: Realm (#{@realm}) does not match (realm: #{credential.server.realm})")
1123+
filter_reason = "Filtered credential #{path} ##{index} reason: Realm (#{@realm}) does not match (realm: #{credential.server.realm})"
1124+
filter_reasons << filter_reason
1125+
wlog(filter_reason)
10951126
next
10961127
end
10971128

10981129
unless !sname || sname.to_s.casecmp?(credential.server.components.snapshot.join('/'))
1099-
wlog("Filtered credential #{file_path} ##{index} reason: SPN (#{sname}) does not match (spn: #{credential.server.components.snapshot.join('/')})")
1130+
filter_reason = "Filtered credential #{path} ##{index} reason: SPN (#{sname}) does not match (spn: #{credential.server.components.snapshot.join('/')})"
1131+
filter_reasons << filter_reason
1132+
wlog(filter_reason)
11001133
next
11011134
end
11021135

11031136
unless !sname_hostname ||
11041137
sname_hostname.to_s.downcase == credential.server.components[1].downcase ||
11051138
sname_hostname.to_s.downcase.ends_with?('.' + credential.server.components[1].downcase)
1106-
wlog("Filtered credential #{file_path} ##{index} reason: SPN (#{sname_hostname}) hostname does not match (spn: #{credential.server.components.snapshot.join('/')})")
1139+
filter_reason = "Filtered credential #{path} ##{index} reason: SPN (#{sname_hostname}) hostname does not match (spn: #{credential.server.components.snapshot.join('/')})"
1140+
filter_reasons << filter_reason
1141+
wlog(filter_reason)
11071142
next
11081143
end
11091144

11101145
unless !@username || @username.casecmp?(credential.client.components.last.to_s)
1111-
wlog("Filtered credential #{file_path} ##{index} reason: Username (#{@username}) does not match (username: #{credential.client.components.last})")
1146+
filter_reason = "Filtered credential #{path} ##{index} reason: Username (#{@username}) does not match (username: #{credential.client.components.last})"
1147+
filter_reasons << filter_reason
1148+
wlog(filter_reason)
11121149
next
11131150
end
11141151

1115-
return credential
1152+
return { credential: credential, filter_reasons: filter_reasons }
11161153
end
11171154

1118-
nil
1155+
{ credential: nil, filter_reasons: filter_reasons }
11191156
end
11201157
end

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ 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
]
4747
)
4848
end
4949

50+
5051
# Alias to return the RHOST datastore option.
5152
#
5253
# @return [String] The current value of RHOST in the datastore.
@@ -68,6 +69,7 @@ def peer
6869
"#{rhost}:#{rport}"
6970
end
7071

72+
7173
# Set the various connection options to use when connecting to the
7274
# target LDAP server based on the current datastore options. Returns
7375
# the resulting connection configuration as a hash.

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 eExamples:
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: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,13 +343,27 @@ def creds_search(*args)
343343

344344
set_rhosts = false
345345
truncate = true
346+
show_id = false
346347

347348
cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ]
348349
delete_count = 0
349350
search_term = nil
350351

351352
while (arg = args.shift)
352353
case arg
354+
# TODO: We should add this to other commands, other options: `-e` `--extended`
355+
# Or from sessions:
356+
# when "-x", "--list-extended"
357+
# show_extended = true
358+
# when "-v", "--list-verbose"
359+
# verbose = true
360+
# I think this has come up before too with the bruteforce PR that required adding Ids that we didn't render
361+
# https://github.com/rapid7/metasploit-framework/issues/17367
362+
# https://github.com/rapid7/metasploit-framework/pull/17601
363+
# There's also the new 'certs' command
364+
when '--id'
365+
cred_table_columns.unshift('id')
366+
show_id = true
353367
when '-o'
354368
output_file = args.shift
355369
if (!output_file)
@@ -506,7 +520,7 @@ def creds_search(*args)
506520
service_info = build_service_info(service)
507521
end
508522
cracked_password_val = cracked_password_core&.private&.data.to_s
509-
tbl << [
523+
row = [
510524
host,
511525
origin,
512526
service_info,
@@ -517,6 +531,11 @@ def creds_search(*args)
517531
jtr_val,
518532
cracked_password_val
519533
]
534+
if show_id
535+
row.unshift(core.id)
536+
end
537+
538+
tbl << row
520539
end
521540
end
522541

0 commit comments

Comments
 (0)