Skip to content

Commit eba2b6c

Browse files
authored
Merge pull request rapid7#19760 from cdelafuente-r7/feat/pkcs12/certs_command/pkinit
Add certs command & use pkinit if kerberos tickets are not available in cache
2 parents a9dc062 + 226853f commit eba2b6c

File tree

13 files changed

+1352
-24
lines changed

13 files changed

+1352
-24
lines changed

lib/metasploit/framework/ldap/client.rb

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def ldap_auth_opts_kerberos(opts, ssl)
5050
auth_opts = {}
5151
raise Msf::ValidationError, 'The LDAP::Rhostname option is required when using Kerberos authentication.' if opts[:ldap_rhostname].blank?
5252
raise Msf::ValidationError, 'The DOMAIN option is required when using Kerberos authentication.' if opts[:domain].blank?
53+
raise Msf::ValidationError, 'The DomainControllerRhost is required when using Kerberos authentication.' if opts[:domain_controller_rhost].blank?
5354

5455
offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(opts[:ldap_krb_offered_enc_types])
5556
raise Msf::ValidationError, 'At least one encryption type is required when using Kerberos authentication.' if offered_etypes.empty?
@@ -112,17 +113,35 @@ def ldap_auth_opts_schannel(opts, ssl)
112113
auth_opts = {}
113114
pfx_path = opts[:ldap_cert_file]
114115
raise Msf::ValidationError, 'The SSL option must be enabled when using Schannel authentication.' unless ssl
115-
raise Msf::ValidationError, 'The LDAP::CertFile option is required when using Schannel authentication.' if pfx_path.blank?
116116
raise Msf::ValidationError, 'Can not sign and seal when using Schannel authentication.' if opts.fetch(:sign_and_seal, false)
117117

118-
unless ::File.file?(pfx_path) && ::File.readable?(pfx_path)
119-
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.'
120-
end
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
128+
else
129+
pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(
130+
framework: opts[:framework],
131+
framework_module: opts[:framework_module]
132+
)
133+
pkcs12_results = pkcs12_storage.pkcs12(
134+
username: opts[:username],
135+
realm: opts[:domain],
136+
tls_auth: true,
137+
status: 'active'
138+
)
139+
if pkcs12_results.empty?
140+
raise Msf::ValidationError, "Pkcs12 for #{opts[:username]}@#{opts[:domain]} not found in the database"
141+
end
121142

122-
begin
123-
pkcs = OpenSSL::PKCS12.new(File.binread(pfx_path), '')
124-
rescue StandardError => e
125-
raise Msf::ValidationError, "Failed to load the PFX file (#{e})"
143+
elog("Using stored certificate for #{opts[:username]}@#{opts[:domain]}")
144+
pkcs = pkcs12_results.first.openssl_pkcs12
126145
end
127146

128147
auth_opts[:auth] = {

lib/metasploit/framework/login_scanner/ldap.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ def each_credential
8787
credential.private = nil
8888
elsif opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::SCHANNEL
8989
# If we're using kerberos auth with schannel then the user/password is irrelevant
90-
# Remove it from the credential so we don't store it
91-
credential.public = nil
90+
# Remove the password from the credential so we don't store it
91+
# Note that the username is kept since it is needed for the certificate lookup.
9292
credential.private = nil
9393
end
9494

lib/msf/core/db_manager/cred.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def update_credential(opts)
246246
if opts[:public][:id]
247247
public_id = opts[:public].delete(:id)
248248
public = Metasploit::Credential::Public.find(public_id)
249-
public.update_attributes(opts[:public])
249+
public.update(opts[:public])
250250
else
251251
public = Metasploit::Credential::Public.where(opts[:public]).first_or_initialize
252252
end
@@ -256,7 +256,7 @@ def update_credential(opts)
256256
if opts[:private][:id]
257257
private_id = opts[:private].delete(:id)
258258
private = Metasploit::Credential::Private.find(private_id)
259-
private.update_attributes(opts[:private])
259+
private.update(opts[:private])
260260
else
261261
private = Metasploit::Credential::Private.where(opts[:private]).first_or_initialize
262262
end
@@ -266,7 +266,7 @@ def update_credential(opts)
266266
if opts[:origin][:id]
267267
origin_id = opts[:origin].delete(:id)
268268
origin = Metasploit::Credential::Origin.find(origin_id)
269-
origin.update_attributes(opts[:origin])
269+
origin.update(opts[:origin])
270270
else
271271
origin = Metasploit::Credential::Origin.where(opts[:origin]).first_or_initialize
272272
end

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,18 @@ def authenticate(options = {})
253253
elsif options[:credential]
254254
auth_context = authenticate_via_krb5_ccache_credential_tgs(options[:credential], options)
255255
else
256+
pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework, framework_module: framework_module)
257+
pkcs12_results = pkcs12_storage.pkcs12(
258+
workspace: workspace,
259+
username: @username,
260+
realm: @realm,
261+
status: 'active'
262+
)
263+
if pkcs12_results.any?
264+
stored_pkcs12 = pkcs12_results.first
265+
options[:pfx] = stored_pkcs12.openssl_pkcs12
266+
print_status("Using stored certificate for #{stored_pkcs12.username}@#{stored_pkcs12.realm}")
267+
end
256268
auth_context = authenticate_via_kdc(options)
257269
auth_context = authenticate_via_krb5_ccache_credential_tgt(auth_context[:credential], options)
258270
end

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ def do_request_cert(icpr, opts)
240240
pkcs12 = OpenSSL::PKCS12.create('', '', private_key, response[:certificate])
241241
# see: https://pki-tutorial.readthedocs.io/en/latest/mime.html#mime-types
242242
info = "#{simple.client.default_domain}\\#{datastore['SMBUser']} Certificate"
243+
# TODO: I was under the impression a single certificate can only have one UPN associated with it.
244+
# But here, `upn` can be an array of UPN's. This will need to be sorted out.
245+
upn_username, upn_domain = upn&.first&.split('@')
243246

244247
service_data = icpr_service_data
245248
credential_data = {
@@ -249,10 +252,12 @@ def do_request_cert(icpr, opts)
249252
protocol: service_data[:proto],
250253
service_name: service_data[:name],
251254
workspace_id: myworkspace_id,
252-
username: upn || datastore['SMBUser'],
255+
username: upn_username || datastore['SMBUser'],
253256
private_type: :pkcs12,
254257
private_data: Base64.strict_encode64(pkcs12.to_der),
255258
private_metadata: { adcs_ca: datastore['CA'], adcs_template: cert_template },
259+
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
260+
realm_value: upn_domain || simple.client.default_domain,
256261
origin_type: :service,
257262
module_fullname: fullname
258263
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
module Msf::Exploit::Remote::Pkcs12
2+
3+
class Storage
4+
include Msf::Auxiliary::Report
5+
6+
# @!attribute [r] framework
7+
# @return [Msf::Framework] the Metasploit framework instance
8+
attr_reader :framework
9+
10+
# @!attribute [r] framework_module
11+
# @return [Msf::Module] the Metasploit framework module that is associated with the authentication instance
12+
attr_reader :framework_module
13+
14+
def initialize(framework: nil, framework_module: nil)
15+
@framework = framework || framework_module&.framework
16+
@framework_module = framework_module
17+
end
18+
19+
# Get stored pkcs12 matching the options query.
20+
#
21+
# @param [Hash] options The options for matching pkcs12's.
22+
# @option options [Integer, Array<Integer>] :id The identifier of the pkcs12 (optional)
23+
# @option options [String] :realm The realm of the pkcs12 (optional)
24+
# @option options [String] :username The username of the pkcs12 (optional)
25+
# @return [Array<StoredPkcs12>]
26+
def pkcs12(options = {}, &block)
27+
stored_pkcs12_array = filter_pkcs12(options).map do |pkcs12_entry|
28+
StoredPkcs12.new(pkcs12_entry)
29+
end
30+
31+
stored_pkcs12_array.each do |stored_pkcs12|
32+
block.call(stored_pkcs12) if block_given?
33+
end
34+
35+
stored_pkcs12_array
36+
end
37+
38+
# Return the raw stored pkcs12.
39+
#
40+
# @param [Hash] options See the options hash description in {#pkcs12}.
41+
# @return [Array<Metasploit::Credential::Core>]
42+
def filter_pkcs12(options)
43+
return [] unless active_db?
44+
45+
filter = {}
46+
filter[:id] = options[:id] if options[:id].present?
47+
48+
creds = framework.db.creds(
49+
workspace: options.fetch(:workspace) { workspace },
50+
type: 'Metasploit::Credential::Pkcs12',
51+
**filter
52+
).select do |cred|
53+
# this is needed since if a filter is provided (e.g. `id:`) framework.db.creds will ignore the type:
54+
next false unless cred.private.type == 'Metasploit::Credential::Pkcs12'
55+
56+
if options[:username].present?
57+
next false if options[:username].casecmp(cred.public.username) != 0
58+
end
59+
60+
if options[:realm].present? && cred.realm
61+
next false if options[:realm].casecmp(cred.realm.value) != 0
62+
end
63+
64+
if options[:status].present?
65+
# If status is not set on the credential, considere it is `active`
66+
status = cred.private.status || 'active'
67+
next false if status != options[:status]
68+
end
69+
70+
cert = cred.private.openssl_pkcs12.certificate
71+
unless Time.now.between?(cert.not_before, cert.not_after)
72+
ilog("[filter_pkcs12] Found a matching certificate but it has expired")
73+
next false
74+
end
75+
76+
if options[:tls_auth]
77+
eku = cert.extensions.select { |c| c.oid == 'extendedKeyUsage' }.first
78+
unless eku&.value.include?('TLS Web Client Authentication')
79+
ilog("[filter_pkcs12] Found a matching certificate but it doesn't have the 'TLS Web Client Authentication' EKU")
80+
next false
81+
end
82+
end
83+
84+
true
85+
end
86+
end
87+
88+
def delete(options = {})
89+
if options.keys == [:ids]
90+
# skip calling #filter_pkcs12 which issues a query when the IDs are specified
91+
ids = options[:ids]
92+
else
93+
ids = filter_pkcs12(options).map(&:id)
94+
end
95+
96+
framework.db.delete_credentials(ids: ids).map do |stored_pkcs12|
97+
StoredPkcs12.new(stored_pkcs12)
98+
end
99+
end
100+
101+
# @return [String] The name of the workspace in which to operate.
102+
def workspace
103+
if @framework_module
104+
return @framework_module.workspace
105+
elsif @framework&.db&.active
106+
return @framework.db.workspace&.name
107+
end
108+
end
109+
110+
# Mark Pkcs12(s) as inactive
111+
#
112+
# @param [Array<Integer>] ids The list of pkcs12 IDs.
113+
# @return [Array<StoredPkcs12>]
114+
def deactivate(ids:)
115+
set_status(ids: ids, status: 'inactive')
116+
end
117+
118+
# Mark Pkcs12(s) as active
119+
#
120+
# @param [Array<Integer>] ids The list of pkcs12 IDs.
121+
# @return [Array<StoredPkcs12>]
122+
def activate(ids:)
123+
set_status(ids: ids, status: 'active')
124+
end
125+
126+
private
127+
128+
# @param [Array<Integer>] ids List of pkcs12 IDs to update
129+
# @param [String] status The status to set for the pkcs12
130+
# @return [Array<StoredPkcs12>]
131+
def set_status(ids:, status:)
132+
updated_pkcs12 = []
133+
ids.each do |id|
134+
pkcs12 = filter_pkcs12({ id: id })
135+
if pkcs12.blank?
136+
print_warning("Pkcs12 with id: #{id} was not found in the database")
137+
next
138+
end
139+
private = pkcs12.first.private
140+
private.metadata.merge!({ 'status' => status } )
141+
updated_pkcs12 << framework.db.update_credential({ id: id, private: { id: private.id, metadata: private.metadata }})
142+
# I know this looks weird but the local db returns a single loot object, remote db returns an array of them
143+
#updated_certs << Array.wrap(framework.db.update_loot({ id: id, info: updated_pkcs12_status })).first
144+
end
145+
updated_pkcs12.map do |stored_pkcs12|
146+
StoredPkcs12.new(stored_pkcs12)
147+
end
148+
end
149+
150+
end
151+
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
module Msf::Exploit::Remote::Pkcs12
2+
3+
class StoredPkcs12
4+
def initialize(pkcs12)
5+
@pkcs12 = pkcs12
6+
end
7+
8+
def id
9+
@pkcs12.id
10+
end
11+
12+
def openssl_pkcs12
13+
private_cred.openssl_pkcs12
14+
end
15+
16+
def adcs_ca
17+
private_cred.adcs_ca || ''
18+
end
19+
20+
def adcs_template
21+
private_cred.adcs_template || ''
22+
end
23+
24+
def private_cred
25+
@pkcs12.private
26+
end
27+
28+
def username
29+
@pkcs12.public&.username || ''
30+
end
31+
32+
def realm
33+
@pkcs12.realm&.value || ''
34+
end
35+
36+
def status
37+
private_cred.status || ''
38+
end
39+
40+
# @return [TrueClass, FalseClass] True if the certificate is valid within the not_before/not_after, false otherwise
41+
def expired?(now = Time.now)
42+
cert = openssl_pkcs12.certificate
43+
!now.between?(cert.not_before, cert.not_after)
44+
end
45+
end
46+
end
47+

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Db
1919
include Msf::Ui::Console::CommandDispatcher::Db::Common
2020
include Msf::Ui::Console::CommandDispatcher::Db::Analyze
2121
include Msf::Ui::Console::CommandDispatcher::Db::Klist
22+
include Msf::Ui::Console::CommandDispatcher::Db::Certs
2223

2324
DB_CONFIG_PATH = 'framework/database'
2425

@@ -49,6 +50,7 @@ def commands
4950
"notes" => "List all notes in the database",
5051
"loot" => "List all loot in the database",
5152
"klist" => "List Kerberos tickets in the database",
53+
"certs" => "List Pkcs12 certificate bundles in the database",
5254
"db_import" => "Import a scan result file (filetype will be auto-detected)",
5355
"db_export" => "Export a file containing the contents of the database",
5456
"db_nmap" => "Executes nmap and records the output automatically",

0 commit comments

Comments
 (0)