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 Dec 20, 2024
1 parent 4c5a365 commit c4d21eb
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 12 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 @@ -221,6 +221,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 @@ -230,14 +233,16 @@ 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: Metasploit::Credential::Pkcs12.build_data(
# pkcs12 is a binary format, but for persisting we Base64 encode it
pkcs12: Base64.strict_encode64(pkcs12.to_der),
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
Loading

0 comments on commit c4d21eb

Please sign in to comment.