Skip to content

Commit

Permalink
Land #17377, Consolidate Kerberos Ticket Storage
Browse files Browse the repository at this point in the history
  • Loading branch information
adfoster-r7 authored Dec 16, 2022
2 parents 6e09236 + 60a76da commit 20496aa
Show file tree
Hide file tree
Showing 21 changed files with 437 additions and 138 deletions.
3 changes: 2 additions & 1 deletion lib/metasploit/framework/mssql/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ def mssql_login(user='sa', pass='', db='', domain_name='')
username: user,
password: pass,
framework: framework,
framework_module: framework_module
framework_module: framework_module,
ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module)
)

kerberos_result = kerberos_authenticator.authenticate
Expand Down
153 changes: 60 additions & 93 deletions lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
# @return [Msf::Framework] the Metasploit framework instance
attr_reader :framework

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

Expand All @@ -61,13 +61,9 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
# @return [String] whether to send delegated creds (from the set Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base::Delegation)
attr_reader :send_delegated_creds

# @!attribute [r] use_cached_credentials
# @return [String] whether to use cached Kerberos credentials from the database
attr_reader :use_cached_credentials

# @!attribute [r] store_credential_cache
# @return [String] whether to store Kerberos TGS MIT Credential Cache to the database
attr_reader :store_credential_cache
# @!attribute [r] ticket_storage
# @return [Msf::Exploit::Remote::Kerberos::Ticket::Storage::Base] the ticket storage driver
attr_reader :ticket_storage

# @!attribute [r] key
# @return [String] the encryption key for authentication
Expand Down Expand Up @@ -112,8 +108,7 @@ def initialize(
mechanism: Rex::Proto::Gss::Mechanism::SPNEGO,
send_delegated_creds: Delegation::ALWAYS,
cache_file: nil,
use_cached_credentials: true,
store_credential_cache: true,
ticket_storage: nil,
key: nil,
etype: nil
)
Expand All @@ -130,8 +125,10 @@ def initialize(
@use_gss_checksum = use_gss_checksum
@mechanism = mechanism
@send_delegated_creds = send_delegated_creds
@use_cached_credentials = use_cached_credentials
@store_credential_cache = store_credential_cache
@ticket_storage = ticket_storage || Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadWrite.new(
framework: framework,
framework_module: framework_module
)
@key = key
@etype = etype

Expand Down Expand Up @@ -175,7 +172,7 @@ def authenticate(options = {})
if @credential
# use an explicit credential
options[:credential] = @credential
elsif use_cached_credentials
else
# load a cached TGS
options[:credential] = get_cached_credential(options)
unless options[:credential]
Expand Down Expand Up @@ -283,49 +280,43 @@ def build_spn(options = {})
end

# @param [Hash] options
# @option options [Boolean] :use_cached_credentials Override the @use_cached_credentials attribute
# @see #authenticate_via_kdc Options dcoumentation
# @option options [Boolean] :ticket_storage Override the @ticket_storage attribute
# @see #authenticate_via_kdc Options documentation
# @see #get_cached_credential Other options documentation
# @return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] The ccache credential
def request_tgt_only(options = {})
credential = nil
if options.fetch(:use_cached_credentials) { use_cached_credentials }
credential = get_cached_credential(
options.merge(
sname: Rex::Proto::Kerberos::Model::PrincipalName.new(
name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
name_string: [
"krbtgt",
realm
]
)
credential = get_cached_credential(
options.merge(
sname: Rex::Proto::Kerberos::Model::PrincipalName.new(
name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
name_string: [
"krbtgt",
realm
]
)
)
if credential
print_status("#{peer} - Using cached credential for #{credential.server} #{credential.client}")
return credential
end
)
if credential
print_status("#{peer} - Using cached credential for #{credential.server} #{credential.client}")
return credential
end

auth_context = authenticate_via_kdc(options)
return auth_context[:credential]
end

# @param [Hash] options
# @option options [Boolean] :use_cached_credentials Override the @use_cached_credentials attribute
# @option options [Boolean] :ticket_storage Override the @ticket_storage attribute
# @param [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] :credential
# The ccache credential from the TGT
# @see #authenticate_via_krb5_ccache_credential_tgt Options dcoumentation
# @see #get_cached_credential Other options dcoumentation
# @return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] The ccache credential
def request_tgs_only(credential, options = {})
if options.fetch(:use_cached_credentials) { use_cached_credentials }
# load a cached TGS
ccache = get_cached_credential(options)
if ccache
print_status("#{peer} - Using cached credential for #{ccache.server} #{ccache.client}")
return ccache
end
# load a cached TGS
if (ccache = get_cached_credential(options))
print_status("#{peer} - Using cached credential for #{ccache.server} #{ccache.client}")
return ccache
end

auth_context = authenticate_via_krb5_ccache_credential_tgt(credential, options)
Expand All @@ -338,7 +329,7 @@ def request_tgs_only(credential, options = {})
# The ccache credential from the TGT
# @param [Hash] options
# @option options [Rex::Proto::Kerberos::Model::PrincipalName] :sname The target service principal name.
# @option options [Boolean] :store_credential_cache Override the @store_credential_cache attribute
# @option options [Boolean] :ticket_storage Override the @ticket_storage attribute
# @option options [String] :impersonate The name of the user to request a ticket on behalf of
# @return [Array] The TGS ticket and the decrypted TGS credentials as a MIT Cache Credential
def s4u2self(credential, options = {})
Expand All @@ -359,7 +350,7 @@ def s4u2self(credential, options = {})
etypes << Rex::Proto::Kerberos::Crypto::Encryption::RC4_HMAC

tgs_options = {
store_credential_cache: options.fetch(:store_credential_cache) { store_credential_cache },
ticket_storage: options.fetch(:ticket_storage, @ticket_storage),
credential_cache_username: options[:impersonate],
pa_data: build_pa_for_user(
{
Expand Down Expand Up @@ -390,7 +381,7 @@ def s4u2self(credential, options = {})
# @option options [Rex::Proto::Kerberos::Model::PrincipalName] :sname The target service principal name.
# @option options [Rex::Proto::Kerberos::Model::Ticket] :tgs_ticket The service ticket to the first service.
# It must have the forwardable flag set. This ticket can be obtained with #s4u2self.
# @option options [Boolean] :store_credential_cache Override the @store_credential_cache attribute
# @option options [Boolean] :ticket_storage Override the @ticket_storage attribute
# @option options [String] :impersonate The name of the user to request a ticket on behalf of
# @return [Array] The new TGS ticket and the decrypted TGS credentials as a MIT Cache Credential
def s4u2proxy(credential, options = {})
Expand Down Expand Up @@ -429,7 +420,7 @@ def s4u2proxy(credential, options = {})
pa_data: pa_data_entry,
additional_flags: [Rex::Proto::Kerberos::Model::KdcOptionFlags::CNAME_IN_ADDL_TKT],
additional_tickets: [options[:tgs_ticket]],
store_credential_cache: options.fetch(:store_credential_cache) { store_credential_cache },
ticket_storage: options.fetch(:ticket_storage, @ticket_storage),
credential_cache_username: options[:impersonate]
}

Expand Down Expand Up @@ -484,14 +475,10 @@ def authenticate_via_kdc(options = {})

print_good("#{peer} - Received a valid TGT-Response")

cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(tgt_result.as_rep, tgt_result.decrypted_part)

if store_credential_cache
path = store_loot('mit.kerberos.ccache', 'application/octet-stream', rhost, cache.encode, nil, loot_info(sname: server_name))
print_status("#{peer} - TGT MIT Credential Cache saved to #{path}")
end
ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(tgt_result.as_rep, tgt_result.decrypted_part)
options.fetch(:ticket_storage, @ticket_storage).store_ccache(ccache, host: rhost)

credential = cache.credentials.first
credential = ccache.credentials.first
session_key = Rex::Proto::Kerberos::Model::EncryptionKey.new(
type: credential.keyblock.enctype.value,
value: credential.keyblock.data.value
Expand Down Expand Up @@ -580,7 +567,7 @@ def authenticate_via_krb5_ccache_credential_tgt(credential, options = {})
etypes = Set.new([ticket.enc_part.etype])
tgs_options = {
pa_data: [],
store_credential_cache: store_credential_cache
ticket_storage: ticket_storage
}

tgs_ticket, tgs_auth = request_service_ticket(
Expand Down Expand Up @@ -787,7 +774,7 @@ def request_delegation_ticket(session_key, tgt_ticket, realm, client_name, tgt_e
# Any additional tickets to add to the request
# @option options [Array<Rex::Proto::Kerberos::Model::PreAuthDataEntry>] :pa_data
# Any additional pre-auth data entries to add to the request
# @option options [Boolean] :store_credential_cache Override the @store_credential_cache attribute
# @option options [Boolean] :ticket_storage Override the @ticket_storage attribute
# @option options [String] :credential_cache_username The name of user
# corresponding to the requested TGS ticket. This name will be used in
# the info field when the tickets is stored in the database. This can be used
Expand Down Expand Up @@ -857,17 +844,22 @@ def request_service_ticket(session_key, tgt_ticket, realm, client_name, etypes,

print_good("#{peer} - Received a valid TGS-Response")

if options.fetch(:store_credential_cache) { store_credential_cache }
cache = extract_kerb_creds(
tgs_res,
session_key.value,
msg_type: Rex::Proto::Kerberos::Crypto::KeyUsage::TGS_REP_ENCPART_SESSION_KEY
)
loot_options = { sname: sname }
loot_options[:username] = options[:credential_cache_username] if options[:credential_cache_username].present?
path = store_loot('mit.kerberos.ccache', 'application/octet-stream', rhost, cache.encode, nil, loot_info(loot_options))
print_status("#{peer} - TGS MIT Credential Cache saved to #{path}")
ccache = extract_kerb_creds(
tgs_res,
session_key.value,
msg_type: Rex::Proto::Kerberos::Crypto::KeyUsage::TGS_REP_ENCPART_SESSION_KEY
)
if options[:credential_cache_username].present?
client = options[:credential_cache_username]
else
client = self.username
end
options.fetch(:ticket_storage, @ticket_storage).store_ccache(
ccache,
host: rhost,
client: client,
server: sname
)

tgs_ticket = tgs_res.ticket
tgs_auth = decrypt_kdc_tgs_rep_enc_part(
Expand All @@ -885,21 +877,13 @@ def request_service_ticket(session_key, tgt_ticket, realm, client_name, etypes,
# @return [Rex::Proto::Kerberos::CredentialCache::Krb5CacheCredential] the credential object for authentication
# @return [nil] returned if the database is not connected or no usable credentials are found
def get_cached_credential(options = {})
return nil unless active_db?

now = Time.now.utc
host = report_host(workspace: myworkspace, host: @host)
framework.db.loot(workspace: myworkspace, host: host, ltype: 'mit.kerberos.ccache', info: loot_info(options)).each do |stored_loot|
ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(stored_loot.data)
# at this time Metasploit stores 1 credential per ccache file, so no need to iterate through them
credential = ccache.credentials.first

tkt_start = credential.starttime == Time.at(0).utc ? credential.authtime : credential.starttime
tkt_end = credential.endtime
return credential if tkt_start < now && now < tkt_end
end

nil
driver = options.fetch(:ticket_storage, @ticket_storage)
driver.load_credential(
host: rhost,
client: options.fetch(:username) { self.username },
server: options.fetch(:sname, nil),
realm: options.fetch(:realm) { self.realm }
)
end

# Load a credential object from a file for authentication. Credentials in the file will be filtered by multiple
Expand Down Expand Up @@ -957,21 +941,4 @@ def load_credential_from_file(file_path, options = {})

nil
end

# Build a loot info string that can later be used in a lookup.
#
# @param [Hash] options
# @option options [Rex::Proto::Kerberos::Model::PrincipalName] :sname the service name (optional)
# @return [String] the info string
def loot_info(options = {})
info = []

info << "realm: #{self.realm.upcase}" if self.realm
info << "serviceName: #{options[:sname].to_s.downcase}" if options[:sname]
username = options.fetch(:username) { self.username }
info << "username: #{username.downcase}" if username

info.join(', ')
end

end
9 changes: 2 additions & 7 deletions lib/msf/core/exploit/remote/kerberos/ticket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,9 @@ def forge_ticket(enc_key:, enc_type:, start_time:, end_time:, sname:, flags:,
ccache = ticket_as_krb5ccache(ticket, opts: opts)

if save_ccache
info = []
info << "realm: #{opts[:realm].upcase}"
info << "serviceName: #{opts[:server].to_s.downcase}"
info << "username: #{opts[:client].to_s.downcase}"

path = store_loot('mit.kerberos.ccache', 'application/octet-stream', domain, ccache.encode, nil, info.join(', '))
print_good("MIT Credential Cache ticket saved on #{path}")
Kerberos::Ticket::Storage.store_ccache(ccache, framework_module: self)
end

ccache
end

Expand Down
65 changes: 65 additions & 0 deletions lib/msf/core/exploit/remote/kerberos/ticket/storage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require 'msf/core/exploit/remote/kerberos/ticket/storage/base'

module Msf::Exploit::Remote::Kerberos::Ticket
module Storage
# Storage a credential cache object.
#
# @param [Hash] options See the options description in Base#tickets.
# @option options [Msf::Module] The framework module associated with the store operation.
def self.store_ccache(ccache, options = {})
driver = WriteOnly.new(framework_module: options[:framework_module])
driver.store_ccache(ccache, options)
end

def initialize(info = {})
super
register_advanced_options(
[
Msf::OptEnum.new(
'KrbCacheMode', [
true,
'Kerberos ticket cache storage mode',
'read-write',
%w[ none read-only write-only read-write ]
]
)
]
)
end

# Build a ticket storage object based on either the specified options or the datastore if no options are defined.
#
# @param [Hash] options Options used to select the ticket storage driver backend. If this option is present, it
# overrides the datastore configuration. All options it contains default to true, meaning it should only be
# necessary to specify the operations (e.g. read) that should be disabled.
# @option options [Boolean] read Whether or not the storage mechanism should support reading
# @option options [Boolean] write Whether or not the storage mechanism should support writing
def kerberos_ticket_storage(options = {})
if options.present?
case [options.fetch(:read, true), options.fetch(:write, true)]
when [false, false]
mode = 'none'
when [false, true]
mode = 'write-only'
when [true, false]
mode = 'read-only'
when [true, true]
mode = 'read-write'
end
else
mode = datastore['KrbCacheMode']
end

case mode
when 'none'
None.new(framework_module: self)
when 'read-only'
ReadOnly.new(framework_module: self)
when 'write-only'
WriteOnly.new(framework_module: self)
when 'read-write'
ReadWrite.new(framework_module: self)
end
end
end
end
Loading

0 comments on commit 20496aa

Please sign in to comment.