Skip to content

Commit

Permalink
Add certs command & use pkinit if kerberos tickets are not available …
Browse files Browse the repository at this point in the history
…in cache
  • Loading branch information
cdelafuente-r7 committed Feb 12, 2025
1 parent 407494c commit 0bfe9c0
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 11 deletions.
33 changes: 25 additions & 8 deletions lib/metasploit/framework/ldap/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def ldap_auth_opts_kerberos(opts, ssl)
auth_opts = {}
raise Msf::ValidationError, 'The LDAP::Rhostname option is required when using Kerberos authentication.' if opts[:ldap_rhostname].blank?
raise Msf::ValidationError, 'The DOMAIN option is required when using Kerberos authentication.' if opts[:domain].blank?
raise Msf::ValidationError, 'The DomainControllerRhost is required when using Kerberos authentication.' if opts[:domain_controller_rhost].blank?

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

unless ::File.file?(pfx_path) && ::File.readable?(pfx_path)
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.'
end
if pfx_path.present?
unless ::File.file?(pfx_path) && ::File.readable?(pfx_path)
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.'
end

begin
pkcs = OpenSSL::PKCS12.new(File.binread(pfx_path), '')
rescue StandardError => e
raise Msf::ValidationError, "Failed to load the PFX file (#{e})"
end
else
pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(
framework: opts[:framework],
framework_module: opts[:framework_module]
)
pkcs12_results = pkcs12_storage.pkcs12(
username: opts[:username],
realm: opts[:domain]
)
if pkcs12_results.empty?
raise Msf::ValidationError, "Pkcs12 for #{opts[:username]}@#{opts[:domain]} not found in the database"
end

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

auth_opts[:auth] = {
Expand Down
4 changes: 2 additions & 2 deletions lib/metasploit/framework/login_scanner/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ def each_credential
credential.private = nil
elsif opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::SCHANNEL
# If we're using kerberos auth with schannel then the user/password is irrelevant
# Remove it from the credential so we don't store it
credential.public = nil
# Remove the password from the credential so we don't store it
# Note that the username is kept since it is needed for the certificate lookup.
credential.private = nil
end

Expand Down
11 changes: 11 additions & 0 deletions lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,17 @@ def authenticate(options = {})
elsif options[:credential]
auth_context = authenticate_via_krb5_ccache_credential_tgs(options[:credential], options)
else
pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework, framework_module: framework_module)
pkcs12_results = pkcs12_storage.pkcs12(
workspace: workspace,
username: @username,
realm: @realm
)
if pkcs12_results.any?
stored_pkcs12 = pkcs12_results.first
options[:pfx] = stored_pkcs12.openssl_pkcs12
print_status("Using stored certificate for #{stored_pkcs12.username}@#{stored_pkcs12.realm}")
end
auth_context = authenticate_via_kdc(options)
auth_context = authenticate_via_krb5_ccache_credential_tgt(auth_context[:credential], options)
end
Expand Down
7 changes: 6 additions & 1 deletion lib/msf/core/exploit/remote/ms_icpr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ def do_request_cert(icpr, opts)
pkcs12 = OpenSSL::PKCS12.create('', '', private_key, response[:certificate])
# see: https://pki-tutorial.readthedocs.io/en/latest/mime.html#mime-types
info = "#{simple.client.default_domain}\\#{datastore['SMBUser']} Certificate"
# TODO: I was under the impression a single certificate can only have one UPN associated with it.
# But here, `upn` can be an array of UPN's. This will need to be sorted out.
upn_username, upn_domain = upn&.first&.split('@')

service_data = icpr_service_data
credential_data = {
Expand All @@ -237,10 +240,12 @@ def do_request_cert(icpr, opts)
protocol: service_data[:proto],
service_name: service_data[:name],
workspace_id: myworkspace_id,
username: upn || datastore['SMBUser'],
username: upn_username || datastore['SMBUser'],
private_type: :pkcs12,
private_data: Base64.strict_encode64(pkcs12.to_der),
private_metadata: { adcs_ca: datastore['CA'], adcs_template: cert_template },
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: upn_domain || simple.client.default_domain,
origin_type: :service,
module_fullname: fullname
}
Expand Down
86 changes: 86 additions & 0 deletions lib/msf/core/exploit/remote/pkcs12/storage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module Msf::Exploit::Remote::Pkcs12

class Storage
include Msf::Auxiliary::Report

# @!attribute [r] framework
# @return [Msf::Framework] the Metasploit framework instance
attr_reader :framework

# @!attribute [r] framework_module
# @return [Msf::Module] the Metasploit framework module that is associated with the authentication instance
attr_reader :framework_module

def initialize(framework: nil, framework_module: nil)
@framework = framework || framework_module&.framework
@framework_module = framework_module
end

# Get stored pkcs12 matching the options query.
#
# @param [Hash] options The options for matching pkcs12's.
# @option options [Integer, Array<Integer>] :id The identifier of the pkcs12 (optional)
# @option options [String] :realm The realm of the pkcs12 (optional)
# @option options [String] :username The username of the pkcs12 (optional)
# @return [Array<StoredPkcs12>]
def pkcs12(options = {}, &block)
stored_pkcs12_array = filter_pkcs12(options).map do |pkcs12_entry|
StoredPkcs12.new(pkcs12_entry)
end

stored_pkcs12_array.each do |stored_pkcs12|
block.call(stored_pkcs12) if block_given?
end

stored_pkcs12_array
end

# Return the raw stored pkcs12.
#
# @param [Hash] options See the options hash description in {#pkcs12}.
# @return [Array<Metasploit::Credential::Core>]
def filter_pkcs12(options)
return [] unless active_db?

filter = {}
filter[:id] = options[:id] if options[:id].present?
filter[:user] = options[:username] if options[:username].present?
filter[:realm] = options[:realm] if options[:realm].present?

creds = framework.db.creds(
workspace: options.fetch(:workspace) { workspace },
type: 'Metasploit::Credential::Pkcs12',
**filter
).select do |cred|
cred.private.type == 'Metasploit::Credential::Pkcs12'
end

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

def delete_pkcs12(options = {})
if options.keys == [:ids]
# skip calling #filter_pkcs12 which issues a query when the IDs are specified
ids = options[:ids]
else
ids = filter_pkcs12(options).map(&:id)
end

framework.db.delete_credentials(ids: ids).map do |stored_pkcs12|
StoredPkcs12.new(stored_pkcs12)
end
end

# @return [String] The name of the workspace in which to operate.
def workspace
if @framework_module
return @framework_module.workspace
elsif @framework&.db&.active
return @framework.db.workspace&.name
end
end

end
end
38 changes: 38 additions & 0 deletions lib/msf/core/exploit/remote/pkcs12/stored_pkcs12.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module Msf::Exploit::Remote::Pkcs12

class StoredPkcs12
def initialize(pkcs12)
@pkcs12 = pkcs12
end

def id
@pkcs12.id
end

def openssl_pkcs12
private_cred.openssl_pkcs12
end

def ca
private_cred.ca
end

def adcs_template
private_cred.adcs_template
end

def private_cred
@pkcs12.private
end

def username
@pkcs12.public.username
end

def realm
@pkcs12.realm.value
end
end

end

2 changes: 2 additions & 0 deletions lib/msf/ui/console/command_dispatcher/db.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Db
include Msf::Ui::Console::CommandDispatcher::Db::Common
include Msf::Ui::Console::CommandDispatcher::Db::Analyze
include Msf::Ui::Console::CommandDispatcher::Db::Klist
include Msf::Ui::Console::CommandDispatcher::Db::Certs

DB_CONFIG_PATH = 'framework/database'

Expand Down Expand Up @@ -49,6 +50,7 @@ def commands
"notes" => "List all notes in the database",
"loot" => "List all loot in the database",
"klist" => "List Kerberos tickets in the database",
"certs" => "List Pkcs12 certificate bundles in the database",
"db_import" => "Import a scan result file (filetype will be auto-detected)",
"db_export" => "Export a file containing the contents of the database",
"db_nmap" => "Executes nmap and records the output automatically",
Expand Down
154 changes: 154 additions & 0 deletions lib/msf/ui/console/command_dispatcher/db/certs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# -*- coding: binary -*-

module Msf::Ui::Console::CommandDispatcher::Db::Certs
#
# Tab completion for the certs command
#
# @param str [String] the string currently being typed before tab was hit
# @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) }
end
end

def cmd_certs_help
print_line 'List Pkcs12 certificate bundles in the database'
print_line 'Usage: certs [options] [username[@domain_upn_format]]'
print_line
print @@certs_opts.usage
print_line
end

@@certs_opts = Rex::Parser::Arguments.new(
['-v', '--verbose'] => [false, 'Verbose output'],
['-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`'],
)

def cmd_certs(*args)
return unless active?

entries_affected = 0
mode = :list
id_search = []
username = nil
verbose = false
@@certs_opts.parse(args) do |opt, _idx, val|
case opt
when '-h', '--help'
cmd_certs_help
return
when '-v', '--verbose'
verbose = true
when '-d', '--delete'
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"
else
# Anything that wasn't an option is a username to search for
username = val
end
end

pkcs12_results = pkcs12_search(username: username, id_search: id_search)

print_line('Pkcs12')
print_line('======')

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

if pkcs12_results.empty?
print_line('No Pkcs12')
print_line
return
end

if verbose
pkcs12_results.each.with_index do |pkcs12_result, index|
print_line "Certificate[#{index}]:"
print_line pkcs12_result.openssl_pkcs12.certificate.to_s
print_line pkcs12_result.openssl_pkcs12.certificate.to_text
print_line
end
else
tbl = Rex::Text::Table.new(
{
'Columns' => ['id', 'username', 'realm', 'subject', 'issuer', 'CA', 'ADCS Template'],
'SortIndex' => -1,
'WordWrap' => false,
'Rows' => pkcs12_results.map do |pkcs12|
[
pkcs12.id,
pkcs12.username,
pkcs12.realm,
pkcs12.openssl_pkcs12.certificate.subject.to_s,
pkcs12.openssl_pkcs12.certificate.issuer.to_s,
pkcs12.ca,
pkcs12.adcs_template
]
end
}
)
print_line(tbl.to_s)
end

if mode == :delete
print_status("Deleted #{entries_affected} #{entries_affected > 1 ? 'entries' : 'entry'}") if entries_affected > 0
end
end


# @param [String, nil] username Search for pkcs12 associated with this username
# @param [Array<Integer>, nil] id_search List of pkcs12 IDs to search for
# @param [Workspace] workspace to search against
# @option [Symbol] :workspace The framework.db.workspace to search against (optional)
# @return [Array<>]
def pkcs12_search(username: nil, id_search: nil, workspace: framework.db.workspace)
pkcs12_results = []

if id_search.present?
begin
pkcs12_results += id_search.flat_map do |id|
pkcs12_storage.pkcs12(
workspace: workspace,
id: id
)
end
rescue ActiveRecord::RecordNotFound => e
wlog("Record Not Found: #{e.message}")
print_warning("Not all records with the ids: #{id_search} could be found.")
print_warning('Please ensure all ids specified are available.')
end
elsif username.present?
realm = nil
if username.include?('@')
username, realm = username.split('@', 2)
end
pkcs12_results += pkcs12_storage.pkcs12(
workspace: workspace,
username: username,
realm: realm
)
else
pkcs12_results += pkcs12_storage.pkcs12(
workspace: workspace
)
end

pkcs12_results.sort_by do |pkcs12|
[pkcs12.realm, pkcs12.username]
end
end

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

end

0 comments on commit 0bfe9c0

Please sign in to comment.