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

Consolidate Kerberos Ticket Storage #17377

Merged
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
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on defaulting to Write behavior by default? 👀

I'm thinking in terms of element of least surprise for existing workflows with tools like impacket, users might not expect the ticket re-use behavior occurring out of the box

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion about the default storage mode right here. If you're instantiating the authenticator yourself, as a module developer to do something specific, it might make sense to just be WriteOnly.

Changing it here wouldn't change the default mode for modules though.

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