Skip to content

Commit e7535d8

Browse files
Add certs command & use pkinit if kerberos tickets are not available in cache
1 parent 31e8c30 commit e7535d8

File tree

8 files changed

+324
-11
lines changed

8 files changed

+324
-11
lines changed

lib/metasploit/framework/ldap/client.rb

Lines changed: 25 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,33 @@ 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+
)
137+
if pkcs12_results.empty?
138+
raise Msf::ValidationError, "Pkcs12 for #{opts[:username]}@#{opts[:domain]} not found in the database"
139+
end
121140

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})"
141+
elog("Using stored certificate for #{opts[:username]}@#{opts[:domain]}")
142+
pkcs = pkcs12_results.first.openssl_pkcs12
126143
end
127144

128145
auth_opts[:auth] = {

lib/metasploit/framework/login_scanner/ldap.rb

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

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,17 @@ 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+
)
262+
if pkcs12_results.any?
263+
stored_pkcs12 = pkcs12_results.first
264+
options[:pfx] = stored_pkcs12.openssl_pkcs12
265+
print_status("Using stored certificate for #{stored_pkcs12.username}@#{stored_pkcs12.realm}")
266+
end
256267
auth_context = authenticate_via_kdc(options)
257268
auth_context = authenticate_via_krb5_ccache_credential_tgt(auth_context[:credential], options)
258269
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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
filter[:user] = options[:username] if options[:username].present?
48+
filter[:realm] = options[:realm] if options[:realm].present?
49+
50+
creds = framework.db.creds(
51+
workspace: options.fetch(:workspace) { workspace },
52+
type: 'Metasploit::Credential::Pkcs12',
53+
**filter
54+
).select do |cred|
55+
cred.private.type == 'Metasploit::Credential::Pkcs12'
56+
end
57+
58+
creds.each do |stored_cred|
59+
block.call(stored_cred) if block_given?
60+
end
61+
end
62+
63+
def delete_pkcs12(options = {})
64+
if options.keys == [:ids]
65+
# skip calling #filter_pkcs12 which issues a query when the IDs are specified
66+
ids = options[:ids]
67+
else
68+
ids = filter_pkcs12(options).map(&:id)
69+
end
70+
71+
framework.db.delete_credentials(ids: ids).map do |stored_pkcs12|
72+
StoredPkcs12.new(stored_pkcs12)
73+
end
74+
end
75+
76+
# @return [String] The name of the workspace in which to operate.
77+
def workspace
78+
if @framework_module
79+
return @framework_module.workspace
80+
elsif @framework&.db&.active
81+
return @framework.db.workspace&.name
82+
end
83+
end
84+
85+
end
86+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 ca
17+
private_cred.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+
end
36+
37+
end
38+

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",
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf::Ui::Console::CommandDispatcher::Db::Certs
4+
#
5+
# Tab completion for the certs command
6+
#
7+
# @param str [String] the string currently being typed before tab was hit
8+
# @param words [Array<String>] the previously completed words on the command line. words is always
9+
# at least 1 when tab completion has reached this stage since the command itself has been completed
10+
def cmd_certs_tabs(str, words)
11+
if words.length == 1
12+
@@certs_opts.option_keys.select { |opt| opt.start_with?(str) }
13+
end
14+
end
15+
16+
def cmd_certs_help
17+
print_line 'List Pkcs12 certificate bundles in the database'
18+
print_line 'Usage: certs [options] [username[@domain_upn_format]]'
19+
print_line
20+
print @@certs_opts.usage
21+
print_line
22+
end
23+
24+
@@certs_opts = Rex::Parser::Arguments.new(
25+
['-v', '--verbose'] => [false, 'Verbose output'],
26+
['-d', '--delete'] => [ false, 'Delete *all* matching pkcs12 entries'],
27+
['-h', '--help'] => [false, 'Help banner'],
28+
['-i', '--index'] => [true, 'Pkcs12 entry ID(s) to search for, e.g. `-i 1` or `-i 1,2,3` or `-i 1 -i 2 -i 3`'],
29+
)
30+
31+
def cmd_certs(*args)
32+
return unless active?
33+
34+
entries_affected = 0
35+
mode = :list
36+
id_search = []
37+
username = nil
38+
verbose = false
39+
@@certs_opts.parse(args) do |opt, _idx, val|
40+
case opt
41+
when '-h', '--help'
42+
cmd_certs_help
43+
return
44+
when '-v', '--verbose'
45+
verbose = true
46+
when '-d', '--delete'
47+
mode = :delete
48+
when '-i', '--id'
49+
id_search = (id_search + val.split(/,\s*|\s+/)).uniq # allows 1 or 1,2,3 or "1 2 3" or "1, 2, 3"
50+
else
51+
# Anything that wasn't an option is a username to search for
52+
username = val
53+
end
54+
end
55+
56+
pkcs12_results = pkcs12_search(username: username, id_search: id_search)
57+
58+
print_line('Pkcs12')
59+
print_line('======')
60+
61+
if mode == :delete
62+
result = pkcs12_storage.delete_pkcs12(ids: pkcs12_results.map(&:id))
63+
entries_affected = result.size
64+
end
65+
66+
if pkcs12_results.empty?
67+
print_line('No Pkcs12')
68+
print_line
69+
return
70+
end
71+
72+
if verbose
73+
pkcs12_results.each.with_index do |pkcs12_result, index|
74+
print_line "Certificate[#{index}]:"
75+
print_line pkcs12_result.openssl_pkcs12.certificate.to_s
76+
print_line pkcs12_result.openssl_pkcs12.certificate.to_text
77+
print_line
78+
end
79+
else
80+
tbl = Rex::Text::Table.new(
81+
{
82+
'Columns' => ['id', 'username', 'realm', 'subject', 'issuer', 'CA', 'ADCS Template'],
83+
'SortIndex' => -1,
84+
'WordWrap' => false,
85+
'Rows' => pkcs12_results.map do |pkcs12|
86+
[
87+
pkcs12.id,
88+
pkcs12.username,
89+
pkcs12.realm,
90+
pkcs12.openssl_pkcs12.certificate.subject.to_s,
91+
pkcs12.openssl_pkcs12.certificate.issuer.to_s,
92+
pkcs12.ca,
93+
pkcs12.adcs_template
94+
]
95+
end
96+
}
97+
)
98+
print_line(tbl.to_s)
99+
end
100+
101+
if mode == :delete
102+
print_status("Deleted #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0
103+
end
104+
end
105+
106+
107+
# @param [String, nil] username Search for pkcs12 associated with this username
108+
# @param [Array<Integer>, nil] id_search List of pkcs12 IDs to search for
109+
# @param [Workspace] workspace to search against
110+
# @option [Symbol] :workspace The framework.db.workspace to search against (optional)
111+
# @return [Array<>]
112+
def pkcs12_search(username: nil, id_search: nil, workspace: framework.db.workspace)
113+
pkcs12_results = []
114+
115+
if id_search.present?
116+
begin
117+
pkcs12_results += id_search.flat_map do |id|
118+
pkcs12_storage.pkcs12(
119+
workspace: workspace,
120+
id: id
121+
)
122+
end
123+
rescue ActiveRecord::RecordNotFound => e
124+
wlog("Record Not Found: #{e.message}")
125+
print_warning("Not all records with the ids: #{id_search} could be found.")
126+
print_warning('Please ensure all ids specified are available.')
127+
end
128+
elsif username.present?
129+
realm = nil
130+
if username.include?('@')
131+
username, realm = username.split('@', 2)
132+
end
133+
pkcs12_results += pkcs12_storage.pkcs12(
134+
workspace: workspace,
135+
username: username,
136+
realm: realm
137+
)
138+
else
139+
pkcs12_results += pkcs12_storage.pkcs12(
140+
workspace: workspace
141+
)
142+
end
143+
144+
pkcs12_results.sort_by do |pkcs12|
145+
[pkcs12.realm, pkcs12.username]
146+
end
147+
end
148+
149+
# @return [Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadWrite]
150+
def pkcs12_storage
151+
@pkcs12_storage ||= Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework)
152+
end
153+
154+
end

0 commit comments

Comments
 (0)