Skip to content

Commit

Permalink
Update certs command, pkcs12 matching and specs
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
cdelafuente-r7 committed Feb 12, 2025
1 parent 0bfe9c0 commit e353684
Show file tree
Hide file tree
Showing 10 changed files with 1,027 additions and 22 deletions.
4 changes: 3 additions & 1 deletion lib/metasploit/framework/ldap/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ def ldap_auth_opts_schannel(opts, ssl)
)
pkcs12_results = pkcs12_storage.pkcs12(
username: opts[:username],
realm: opts[:domain]
realm: opts[:domain],
tls_auth: true,
status: 'active'
)
if pkcs12_results.empty?
raise Msf::ValidationError, "Pkcs12 for #{opts[:username]}@#{opts[:domain]} not found in the database"
Expand Down
6 changes: 3 additions & 3 deletions lib/msf/core/db_manager/cred.rb
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def update_credential(opts)
if opts[:public][:id]
public_id = opts[:public].delete(:id)
public = Metasploit::Credential::Public.find(public_id)
public.update_attributes(opts[:public])
public.update(opts[:public])
else
public = Metasploit::Credential::Public.where(opts[:public]).first_or_initialize
end
Expand All @@ -256,7 +256,7 @@ def update_credential(opts)
if opts[:private][:id]
private_id = opts[:private].delete(:id)
private = Metasploit::Credential::Private.find(private_id)
private.update_attributes(opts[:private])
private.update(opts[:private])
else
private = Metasploit::Credential::Private.where(opts[:private]).first_or_initialize
end
Expand All @@ -266,7 +266,7 @@ def update_credential(opts)
if opts[:origin][:id]
origin_id = opts[:origin].delete(:id)
origin = Metasploit::Credential::Origin.find(origin_id)
origin.update_attributes(opts[:origin])
origin.update(opts[:origin])
else
origin = Metasploit::Credential::Origin.where(opts[:origin]).first_or_initialize
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ def authenticate(options = {})
pkcs12_results = pkcs12_storage.pkcs12(
workspace: workspace,
username: @username,
realm: @realm
realm: @realm,
status: 'active'
)
if pkcs12_results.any?
stored_pkcs12 = pkcs12_results.first
Expand Down
68 changes: 63 additions & 5 deletions lib/msf/core/exploit/remote/pkcs12/storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,33 @@ def filter_pkcs12(options)
type: 'Metasploit::Credential::Pkcs12',
**filter
).select do |cred|
cred.private.type == 'Metasploit::Credential::Pkcs12'
end
# this is needed since if a filter is provided (e.g. `id:`) framework.db.creds will ignore the type:
next false unless cred.private.type == 'Metasploit::Credential::Pkcs12'
if options[:status].present?
# If status is not set on the credential, considere it is `active`
status = cred.private.status || 'active'
next false if status != options[:status]
end

cert = cred.private.openssl_pkcs12.certificate
unless Time.now.between?(cert.not_before, cert.not_after)
ilog("[filter_pkcs12] Found a matching certificate but it has expired")
next false
end

if options[:tls_auth]
eku = cert.extensions.select { |c| c.oid == 'extendedKeyUsage' }.first
unless eku&.value == 'TLS Web Client Authentication'
ilog("[filter_pkcs12] Found a matching certificate but it doesn't have the 'TLS Web Client Authentication' EKU")
next false
end
end

creds.each do |stored_cred|
block.call(stored_cred) if block_given?
true
end
end

def delete_pkcs12(options = {})
def delete(options = {})
if options.keys == [:ids]
# skip calling #filter_pkcs12 which issues a query when the IDs are specified
ids = options[:ids]
Expand All @@ -82,5 +100,45 @@ def workspace
end
end

# Mark Pkcs12(s) as inactive
#
# @param [Array<Integer>] ids The list of pkcs12 IDs.
# @return [Array<StoredPkcs12>]
def deactivate(ids:)
set_status(ids: ids, status: 'inactive')
end

# Mark Pkcs12(s) as active
#
# @param [Array<Integer>] ids The list of pkcs12 IDs.
# @return [Array<StoredPkcs12>]
def activate(ids:)
set_status(ids: ids, status: 'active')
end

private

# @param [Array<Integer>] ids List of pkcs12 IDs to update
# @param [String] status The status to set for the pkcs12
# @return [Array<StoredPkcs12>]
def set_status(ids:, status:)
updated_pkcs12 = []
ids.each do |id|
pkcs12 = filter_pkcs12({ id: id })
if pkcs12.blank?
print_warning("Pkcs12 with id: #{id} was not found in the database")
next
end
private = pkcs12.first.private
private.metadata.merge!({ 'status' => status } )
updated_pkcs12 << framework.db.update_credential({ id: id, private: { id: private.id, metadata: private.metadata }})
# I know this looks weird but the local db returns a single loot object, remote db returns an array of them
#updated_certs << Array.wrap(framework.db.update_loot({ id: id, info: updated_pkcs12_status })).first
end
updated_pkcs12.map do |stored_pkcs12|
StoredPkcs12.new(stored_pkcs12)
end
end

end
end
15 changes: 12 additions & 3 deletions lib/msf/core/exploit/remote/pkcs12/stored_pkcs12.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def openssl_pkcs12
private_cred.openssl_pkcs12
end

def ca
private_cred.ca
def adcs_ca
private_cred.adcs_ca
end

def adcs_template
Expand All @@ -32,7 +32,16 @@ def username
def realm
@pkcs12.realm.value
end
end

def status
private_cred.status
end

# @return [TrueClass, FalseClass] True if the certificate is valid within the not_before/not_after, false otherwise
def expired?(now = Time.now)
cert = openssl_pkcs12.certificate
!now.between?(cert.not_before, cert.not_after)
end
end
end

90 changes: 83 additions & 7 deletions lib/msf/ui/console/command_dispatcher/db/certs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ module Msf::Ui::Console::CommandDispatcher::Db::Certs
# @param words [Array<String>] the previously completed words on the command line. words is always
# at least 1 when tab completion has reached this stage since the command itself has been completed
def cmd_certs_tabs(str, words)
if words.length == 1
@@certs_opts.option_keys.select { |opt| opt.start_with?(str) }
tabs = []

case words.length
when 1
tabs = @@certs_opts.option_keys.select { |opt| opt.start_with?(str) }
when 2
tabs = if words[1] == '-e' || words[1] == '--export'
tab_complete_filenames(str, words)
else
[]
end
end

tabs
end

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

def cmd_certs(*args)
Expand All @@ -36,6 +50,7 @@ def cmd_certs(*args)
id_search = []
username = nil
verbose = false
export_path = nil
@@certs_opts.parse(args) do |opt, _idx, val|
case opt
when '-h', '--help'
Expand All @@ -47,6 +62,12 @@ def cmd_certs(*args)
mode = :delete
when '-i', '--id'
id_search = (id_search + val.split(/,\s*|\s+/)).uniq # allows 1 or 1,2,3 or "1 2 3" or "1, 2, 3"
when '-a', '--activate'
mode = :activate
when '-A', '--deactivate'
mode = :deactivate
when '-e', '--export'
export_path = val
else
# Anything that wasn't an option is a username to search for
username = val
Expand All @@ -59,10 +80,30 @@ def cmd_certs(*args)
print_line('======')

if mode == :delete
result = pkcs12_storage.delete_pkcs12(ids: pkcs12_results.map(&:id))
result = pkcs12_storage.delete(ids: pkcs12_results.map(&:id))
entries_affected = result.size
end

if mode == :activate || mode == :deactivate
pkcs12_results = set_pkcs12_status(mode, pkcs12_results)
entries_affected = pkcs12_results.size
end

if export_path
if pkcs12_results.empty?
print_error('No mathing Pkcs12 entry to export')
return
end
if pkcs12_results.size > 1
print_error('More than one mathing Pkcs12 entry found. Filter with `-i` and/or provide a username')
return
end

raw_data = Base64.strict_decode64(pkcs12_results.first.private_cred.data)
::File.binwrite(::File.expand_path(export_path), raw_data)
return
end

if pkcs12_results.empty?
print_line('No Pkcs12')
print_line
Expand All @@ -79,7 +120,7 @@ def cmd_certs(*args)
else
tbl = Rex::Text::Table.new(
{
'Columns' => ['id', 'username', 'realm', 'subject', 'issuer', 'CA', 'ADCS Template'],
'Columns' => ['id', 'username', 'realm', 'subject', 'issuer', 'ADCS CA', 'ADCS Template', 'status'],
'SortIndex' => -1,
'WordWrap' => false,
'Rows' => pkcs12_results.map do |pkcs12|
Expand All @@ -89,17 +130,23 @@ def cmd_certs(*args)
pkcs12.realm,
pkcs12.openssl_pkcs12.certificate.subject.to_s,
pkcs12.openssl_pkcs12.certificate.issuer.to_s,
pkcs12.ca,
pkcs12.adcs_template
pkcs12.adcs_ca,
pkcs12.adcs_template,
pkcs12_status(pkcs12)
]
end
}
)
print_line(tbl.to_s)
end

if mode == :delete
case mode
when :delete
print_status("Deleted #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0
when :activate
print_status("Activated #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0
when :deactivate
print_status("Deactivated #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0
end
end

Expand Down Expand Up @@ -146,9 +193,38 @@ def pkcs12_search(username: nil, id_search: nil, workspace: framework.db.workspa
end
end


private

# @return [Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadWrite]
def pkcs12_storage
@pkcs12_storage ||= Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework)
end

# Gets the status of a Pkcs12
#
# @param [Msf::Exploit::Remote::Pkcs12::Storage]
# @return [String] Status of the Pkcs12
def pkcs12_status(pkcs12)
if pkcs12.expired?
'>>expired<<'
elsif pkcs12.status.blank?
'active'
else
pkcs12.status
end
end

# Sets the status of the Pkcs12
#
# @param [Symbol] mode The status (:activate or :deactivate) to apply to the Pkcs12(s)
# @param [Array<StoredPkcs12>] tickets The Pkcs12 which statuses are to be updated
# @return [Array<StoredPkcs12>]
def set_pkcs12_status(mode, pkcs12)
if mode == :activate
pkcs12_storage.activate(ids: pkcs12.map(&:id))
elsif mode == :deactivate
pkcs12_storage.deactivate(ids: pkcs12.map(&:id))
end
end
end
5 changes: 3 additions & 2 deletions modules/auxiliary/scanner/ldap/ldap_login.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,10 @@ def run_host(ip)
successful_logins << result
if opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::SCHANNEL
# Schannel auth has no meaningful credential information to store in the DB
print_brute level: :good, ip: ip, msg: "Success: 'Cert File #{opts[:ldap_cert_file]}'"
msg = opts[:ldap_cert_file].nil? ? 'Using stored certificate' : "Cert File #{opts[:ldap_cert_file]}"
print_brute level: :good, ip: ip, msg: "Success: '#{msg}'"
else
create_credential_and_login(credential_data)
create_credential_and_login(credential_data) if result.credential.private
print_brute level: :good, ip: ip, msg: "Success: '#{result.credential}'"
end
successful_sessions << create_session(result, ip) if create_session?
Expand Down
Loading

0 comments on commit e353684

Please sign in to comment.