Skip to content

Commit 630c2c0

Browse files
Update certs command, pkcs12 matching and specs
- use the `status`, certificate's `not_before`/`not_after` and check if the TLS OID is present to filter pkcs12 before using them with PKInit - add the `activate`, `deactivate` and `export` capabilities to the certs command - add specs
1 parent e7535d8 commit 630c2c0

File tree

10 files changed

+1027
-22
lines changed

10 files changed

+1027
-22
lines changed

lib/metasploit/framework/ldap/client.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ def ldap_auth_opts_schannel(opts, ssl)
132132
)
133133
pkcs12_results = pkcs12_storage.pkcs12(
134134
username: opts[:username],
135-
realm: opts[:domain]
135+
realm: opts[:domain],
136+
tls_auth: true,
137+
status: 'active'
136138
)
137139
if pkcs12_results.empty?
138140
raise Msf::ValidationError, "Pkcs12 for #{opts[:username]}@#{opts[:domain]} not found in the database"

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ def authenticate(options = {})
257257
pkcs12_results = pkcs12_storage.pkcs12(
258258
workspace: workspace,
259259
username: @username,
260-
realm: @realm
260+
realm: @realm,
261+
status: 'active'
261262
)
262263
if pkcs12_results.any?
263264
stored_pkcs12 = pkcs12_results.first

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

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,33 @@ def filter_pkcs12(options)
5252
type: 'Metasploit::Credential::Pkcs12',
5353
**filter
5454
).select do |cred|
55-
cred.private.type == 'Metasploit::Credential::Pkcs12'
56-
end
55+
# this is needed since if a filter is provided (e.g. `id:`) framework.db.creds will ignore the type:
56+
next false unless cred.private.type == 'Metasploit::Credential::Pkcs12'
57+
if options[:status].present?
58+
# If status is not set on the credential, considere it is `active`
59+
status = cred.private.status || 'active'
60+
next false if status != options[:status]
61+
end
62+
63+
cert = cred.private.openssl_pkcs12.certificate
64+
unless Time.now.between?(cert.not_before, cert.not_after)
65+
ilog("[filter_pkcs12] Found a matching certificate but it has expired")
66+
next false
67+
end
68+
69+
if options[:tls_auth]
70+
eku = cert.extensions.select { |c| c.oid == 'extendedKeyUsage' }.first
71+
unless eku&.value == 'TLS Web Client Authentication'
72+
ilog("[filter_pkcs12] Found a matching certificate but it doesn't have the 'TLS Web Client Authentication' EKU")
73+
next false
74+
end
75+
end
5776

58-
creds.each do |stored_cred|
59-
block.call(stored_cred) if block_given?
77+
true
6078
end
6179
end
6280

63-
def delete_pkcs12(options = {})
81+
def delete(options = {})
6482
if options.keys == [:ids]
6583
# skip calling #filter_pkcs12 which issues a query when the IDs are specified
6684
ids = options[:ids]
@@ -82,5 +100,45 @@ def workspace
82100
end
83101
end
84102

103+
# Mark Pkcs12(s) as inactive
104+
#
105+
# @param [Array<Integer>] ids The list of pkcs12 IDs.
106+
# @return [Array<StoredPkcs12>]
107+
def deactivate(ids:)
108+
set_status(ids: ids, status: 'inactive')
109+
end
110+
111+
# Mark Pkcs12(s) as active
112+
#
113+
# @param [Array<Integer>] ids The list of pkcs12 IDs.
114+
# @return [Array<StoredPkcs12>]
115+
def activate(ids:)
116+
set_status(ids: ids, status: 'active')
117+
end
118+
119+
private
120+
121+
# @param [Array<Integer>] ids List of pkcs12 IDs to update
122+
# @param [String] status The status to set for the pkcs12
123+
# @return [Array<StoredPkcs12>]
124+
def set_status(ids:, status:)
125+
updated_pkcs12 = []
126+
ids.each do |id|
127+
pkcs12 = filter_pkcs12({ id: id })
128+
if pkcs12.blank?
129+
print_warning("Pkcs12 with id: #{id} was not found in the database")
130+
next
131+
end
132+
private = pkcs12.first.private
133+
private.metadata.merge!({ 'status' => status } )
134+
updated_pkcs12 << framework.db.update_credential({ id: id, private: { id: private.id, metadata: private.metadata }})
135+
# I know this looks weird but the local db returns a single loot object, remote db returns an array of them
136+
#updated_certs << Array.wrap(framework.db.update_loot({ id: id, info: updated_pkcs12_status })).first
137+
end
138+
updated_pkcs12.map do |stored_pkcs12|
139+
StoredPkcs12.new(stored_pkcs12)
140+
end
141+
end
142+
85143
end
86144
end

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ def openssl_pkcs12
1313
private_cred.openssl_pkcs12
1414
end
1515

16-
def ca
17-
private_cred.ca
16+
def adcs_ca
17+
private_cred.adcs_ca
1818
end
1919

2020
def adcs_template
@@ -32,7 +32,16 @@ def username
3232
def realm
3333
@pkcs12.realm.value
3434
end
35-
end
3635

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
3746
end
3847

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

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,20 @@ module Msf::Ui::Console::CommandDispatcher::Db::Certs
88
# @param words [Array<String>] the previously completed words on the command line. words is always
99
# at least 1 when tab completion has reached this stage since the command itself has been completed
1010
def cmd_certs_tabs(str, words)
11-
if words.length == 1
12-
@@certs_opts.option_keys.select { |opt| opt.start_with?(str) }
11+
tabs = []
12+
13+
case words.length
14+
when 1
15+
tabs = @@certs_opts.option_keys.select { |opt| opt.start_with?(str) }
16+
when 2
17+
tabs = if words[1] == '-e' || words[1] == '--export'
18+
tab_complete_filenames(str, words)
19+
else
20+
[]
21+
end
1322
end
23+
24+
tabs
1425
end
1526

1627
def cmd_certs_help
@@ -26,6 +37,9 @@ def cmd_certs_help
2637
['-d', '--delete'] => [ false, 'Delete *all* matching pkcs12 entries'],
2738
['-h', '--help'] => [false, 'Help banner'],
2839
['-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`'],
40+
['-a', '--activate'] => [false, 'Activates *all* matching pkcs12 entries'],
41+
['-A', '--deactivate'] => [false, 'Deactivates *all* matching pkcs12 entries'],
42+
['-e', '--export'] => [true, 'The file path where to export the matching pkcs12 entry']
2943
)
3044

3145
def cmd_certs(*args)
@@ -36,6 +50,7 @@ def cmd_certs(*args)
3650
id_search = []
3751
username = nil
3852
verbose = false
53+
export_path = nil
3954
@@certs_opts.parse(args) do |opt, _idx, val|
4055
case opt
4156
when '-h', '--help'
@@ -47,6 +62,12 @@ def cmd_certs(*args)
4762
mode = :delete
4863
when '-i', '--id'
4964
id_search = (id_search + val.split(/,\s*|\s+/)).uniq # allows 1 or 1,2,3 or "1 2 3" or "1, 2, 3"
65+
when '-a', '--activate'
66+
mode = :activate
67+
when '-A', '--deactivate'
68+
mode = :deactivate
69+
when '-e', '--export'
70+
export_path = val
5071
else
5172
# Anything that wasn't an option is a username to search for
5273
username = val
@@ -59,10 +80,30 @@ def cmd_certs(*args)
5980
print_line('======')
6081

6182
if mode == :delete
62-
result = pkcs12_storage.delete_pkcs12(ids: pkcs12_results.map(&:id))
83+
result = pkcs12_storage.delete(ids: pkcs12_results.map(&:id))
6384
entries_affected = result.size
6485
end
6586

87+
if mode == :activate || mode == :deactivate
88+
pkcs12_results = set_pkcs12_status(mode, pkcs12_results)
89+
entries_affected = pkcs12_results.size
90+
end
91+
92+
if export_path
93+
if pkcs12_results.empty?
94+
print_error('No mathing Pkcs12 entry to export')
95+
return
96+
end
97+
if pkcs12_results.size > 1
98+
print_error('More than one mathing Pkcs12 entry found. Filter with `-i` and/or provide a username')
99+
return
100+
end
101+
102+
raw_data = Base64.strict_decode64(pkcs12_results.first.private_cred.data)
103+
::File.binwrite(::File.expand_path(export_path), raw_data)
104+
return
105+
end
106+
66107
if pkcs12_results.empty?
67108
print_line('No Pkcs12')
68109
print_line
@@ -79,7 +120,7 @@ def cmd_certs(*args)
79120
else
80121
tbl = Rex::Text::Table.new(
81122
{
82-
'Columns' => ['id', 'username', 'realm', 'subject', 'issuer', 'CA', 'ADCS Template'],
123+
'Columns' => ['id', 'username', 'realm', 'subject', 'issuer', 'ADCS CA', 'ADCS Template', 'status'],
83124
'SortIndex' => -1,
84125
'WordWrap' => false,
85126
'Rows' => pkcs12_results.map do |pkcs12|
@@ -89,17 +130,23 @@ def cmd_certs(*args)
89130
pkcs12.realm,
90131
pkcs12.openssl_pkcs12.certificate.subject.to_s,
91132
pkcs12.openssl_pkcs12.certificate.issuer.to_s,
92-
pkcs12.ca,
93-
pkcs12.adcs_template
133+
pkcs12.adcs_ca,
134+
pkcs12.adcs_template,
135+
pkcs12_status(pkcs12)
94136
]
95137
end
96138
}
97139
)
98140
print_line(tbl.to_s)
99141
end
100142

101-
if mode == :delete
143+
case mode
144+
when :delete
102145
print_status("Deleted #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0
146+
when :activate
147+
print_status("Activated #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0
148+
when :deactivate
149+
print_status("Deactivated #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0
103150
end
104151
end
105152

@@ -146,9 +193,38 @@ def pkcs12_search(username: nil, id_search: nil, workspace: framework.db.workspa
146193
end
147194
end
148195

196+
197+
private
198+
149199
# @return [Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadWrite]
150200
def pkcs12_storage
151201
@pkcs12_storage ||= Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework)
152202
end
153203

204+
# Gets the status of a Pkcs12
205+
#
206+
# @param [Msf::Exploit::Remote::Pkcs12::Storage]
207+
# @return [String] Status of the Pkcs12
208+
def pkcs12_status(pkcs12)
209+
if pkcs12.expired?
210+
'>>expired<<'
211+
elsif pkcs12.status.blank?
212+
'active'
213+
else
214+
pkcs12.status
215+
end
216+
end
217+
218+
# Sets the status of the Pkcs12
219+
#
220+
# @param [Symbol] mode The status (:activate or :deactivate) to apply to the Pkcs12(s)
221+
# @param [Array<StoredPkcs12>] tickets The Pkcs12 which statuses are to be updated
222+
# @return [Array<StoredPkcs12>]
223+
def set_pkcs12_status(mode, pkcs12)
224+
if mode == :activate
225+
pkcs12_storage.activate(ids: pkcs12.map(&:id))
226+
elsif mode == :deactivate
227+
pkcs12_storage.deactivate(ids: pkcs12.map(&:id))
228+
end
229+
end
154230
end

modules/auxiliary/scanner/ldap/ldap_login.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ def run_host(ip)
164164
successful_logins << result
165165
if opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::SCHANNEL
166166
# Schannel auth has no meaningful credential information to store in the DB
167-
print_brute level: :good, ip: ip, msg: "Success: 'Cert File #{opts[:ldap_cert_file]}'"
167+
msg = opts[:ldap_cert_file].nil? ? 'Using stored certificate' : "Cert File #{opts[:ldap_cert_file]}"
168+
print_brute level: :good, ip: ip, msg: "Success: '#{msg}'"
168169
else
169-
create_credential_and_login(credential_data)
170+
create_credential_and_login(credential_data) if result.credential.private
170171
print_brute level: :good, ip: ip, msg: "Success: '#{result.credential}'"
171172
end
172173
successful_sessions << create_session(result, ip) if create_session?

0 commit comments

Comments
 (0)