Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add certs command & use pkinit if kerberos tickets are not available in cache #19760

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ source 'https://rubygems.org'
# spec.add_runtime_dependency '<name>', [<version requirements>]
gemspec name: 'metasploit-framework'

gem 'metasploit-credential', git: 'https://github.com/cdelafuente-r7/metasploit-credential', branch: 'enh/MS-9710/add_pkcs12_metadata'

# separate from test as simplecov is not run on travis-ci
group :coverage do
# code coverage for tests
Expand Down
27 changes: 17 additions & 10 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
GIT
remote: https://github.com/cdelafuente-r7/metasploit-credential
revision: acc5a012f4bc7e7774af059e778b947cd994da1e
branch: enh/MS-9710/add_pkcs12_metadata
specs:
metasploit-credential (6.0.12)
metasploit-concern
metasploit-model
metasploit_data_models (>= 5.0.0)
net-ssh
pg
railties
rex-socket
rubyntlm
rubyzip

PATH
remote: .
specs:
Expand Down Expand Up @@ -286,16 +302,6 @@ GEM
activesupport (~> 7.0)
railties (~> 7.0)
zeitwerk
metasploit-credential (6.0.11)
metasploit-concern
metasploit-model
metasploit_data_models (>= 5.0.0)
net-ssh
pg
railties
rex-socket
rubyntlm
rubyzip
metasploit-model (5.0.2)
activemodel (~> 7.0)
activesupport (~> 7.0)
Expand Down Expand Up @@ -581,6 +587,7 @@ DEPENDENCIES
factory_bot_rails
fivemat
memory_profiler
metasploit-credential!
metasploit-framework!
octokit
pry-byebug
Expand Down
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
15 changes: 12 additions & 3 deletions 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,10 +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,
# pkcs12 is a binary format, but for persisting we Base64 encode it
private_data: Base64.strict_encode64(pkcs12.to_der),
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

34 changes: 21 additions & 13 deletions lib/msf/ui/console/command_dispatcher/creds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,18 @@ def cmd_creds_help
print_line "Usage - Adding credentials:"
print_line " creds add uses the following named parameters."
{
user: 'Public, usually a username',
password: 'Private, private_type Password.',
ntlm: 'Private, private_type NTLM Hash.',
postgres: 'Private, private_type postgres MD5',
pkcs12: 'Private, private_type pkcs12 archive file, must be a file path.',
'ssh-key' => 'Private, private_type SSH key, must be a file path.',
hash: 'Private, private_type Nonreplayable hash',
jtr: 'Private, private_type John the Ripper hash type.',
realm: 'Realm, ',
'realm-type'=>"Realm, realm_type (#{Metasploit::Model::Realm::Key::SHORT_NAMES.keys.join(' ')}), defaults to domain."
user: 'Public, usually a username',
password: 'Private, private_type Password.',
ntlm: 'Private, private_type NTLM Hash.',
postgres: 'Private, private_type postgres MD5',
pkcs12: 'Private, private_type pkcs12 archive file, must be a file path.',
'ssh-key' => 'Private, private_type SSH key, must be a file path.',
hash: 'Private, private_type Nonreplayable hash',
jtr: 'Private, private_type John the Ripper hash type.',
realm: 'Realm, ',
'realm-type' => "Realm, realm_type (#{Metasploit::Model::Realm::Key::SHORT_NAMES.keys.join(' ')}), defaults to domain.",
ca: 'CA, Certificate Authority that issued the pkcs12 certificate',
'adcs-template' => 'ADCS Template, template used to issue the pkcs12 certificate'
}.each_pair do |keyword, description|
print_line " #{keyword.to_s.ljust 10}: #{description}"
end
Expand Down Expand Up @@ -206,7 +208,7 @@ def creds_add(*args)
end

begin
params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','address','port','protocol', 'service-name', 'jtr', 'pkcs12', 'postgres')
params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','address','port','protocol', 'service-name', 'jtr', 'pkcs12', 'postgres', 'ca', 'adcs-template')
rescue ArgumentError => e
print_error(e.message)
end
Expand Down Expand Up @@ -275,7 +277,11 @@ def creds_add(*args)
print_error("Failed to add pkcs12 archive: #{e}")
end
data[:private_type] = :pkcs12
data[:private_data] = pkcs12_data
data[:private_data] = Metasploit::Credential::Pkcs12.build_data(
pkcs12: pkcs12_data,
ca: params['ca'],
adcs_template: params['adcs-template']
)
end

if params.key? 'hash'
Expand Down Expand Up @@ -414,11 +420,13 @@ def creds_search(*args)
when 'password'
Metasploit::Credential::Password
when 'hash'
Metasploit::Credential::PasswordHash
Metasploit::Credential::NonreplayableHash
when 'ntlm'
Metasploit::Credential::NTLMHash
when 'KrbEncKey'.downcase
Metasploit::Credential::KrbEncKey
when 'pkcs12'
Metasploit::Credential::Pkcs12
when *Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS
opts[:jtr_format] = ptype
Metasploit::Credential::NonreplayableHash
Expand Down
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
Loading