From 5f52ebeea73005c5028c1df1655eccee8eedc606 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 13 Dec 2022 12:02:17 -0500 Subject: [PATCH 1/8] Consolidate the loot_info UID string --- .../kerberos/service_authenticator/base.rb | 18 +++++----- .../core/exploit/remote/kerberos/ticket.rb | 33 +++++++++++++++---- .../auxiliary/admin/kerberos/pkinit_login.rb | 12 ++++--- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb index 1bd543bcd82a..137ef0ce6fde 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -961,17 +961,15 @@ def load_credential_from_file(file_path, options = {}) # 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) + # @option options [String] :realm the ticket realm + # @option options [String] :sname the ticket service name + # @option options [String] :username the ticket username # @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(', ') + Msf::Exploit::Remote::Kerberos::Ticket.loot_info( + client: options.fetch(:username) { self.username }, + server: options.fetch(:sname, nil), + realm: options.fetch(:realm) { self.realm } + ) end - end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket.rb b/lib/msf/core/exploit/remote/kerberos/ticket.rb index d77e613d9e8d..8ef76af477b6 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket.rb @@ -63,12 +63,8 @@ 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(', ')) + loot_info = Kerberos::Ticket.loot_info(opts) + path = store_loot('mit.kerberos.ccache', 'application/octet-stream', domain, ccache.encode, nil, loot_info) print_good("MIT Credential Cache ticket saved on #{path}") end ccache @@ -183,6 +179,31 @@ def print_ccache_contents(ccache, key: nil) presenter = Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(ccache) print_status presenter.present(key: key) end + + # Build a loot info string that can later be used in a lookup. + # + # @param [Hash] options + # @option options [String] :realm the realm of the ticket (optional) + # @option options [String] :server the service name of the ticket (optional) + # @option options [String] :client the client username of the ticket (optional) + # @option options [Boolean] :valid whether or not the ticket is valid, defaults to true (optional) + # @return [String] the info string + def self.loot_info(options = {}) + info = [] + + info << '[invalid]' if !options.fetch(:valid, true) + + realm = options[:realm] + info << "realm: #{realm.to_s.upcase}" if realm.present? + + client = options[:client] + info << "client: #{client.to_s.downcase}" if client.present? + + server = options[:server] + info << "server: #{server.to_s.downcase}" if server.present? + + info.join(', ') + end end end end diff --git a/modules/auxiliary/admin/kerberos/pkinit_login.rb b/modules/auxiliary/admin/kerberos/pkinit_login.rb index e26612666724..842e74a4002e 100644 --- a/modules/auxiliary/admin/kerberos/pkinit_login.rb +++ b/modules/auxiliary/admin/kerberos/pkinit_login.rb @@ -72,10 +72,12 @@ def run print_good('Successfully authenticated with certificate') enc_part = decrypt_kdc_as_rep_enc_part(tgt_result.as_rep, key) - info = [] - info << "realm: #{realm.upcase}" - info << "serviceName: #{server_name.downcase}" - info << "username: #{username.downcase}" + loot_info = Msf::Exploit::Remote::Kerberos::Ticket.loot_info( + client: username, + server: server_name, + realm: realm, + valid: true + ) report_service( host: rhost, @@ -86,7 +88,7 @@ def run ) ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(tgt_result.as_rep, enc_part) - path = store_loot('mit.kerberos.ccache', 'application/octet-stream', rhost, ccache.encode, nil, info.join(', ')) + path = store_loot('mit.kerberos.ccache', 'application/octet-stream', rhost, ccache.encode, nil, loot_info) print_status("#{peer} - TGT MIT Credential Cache saved to #{path}") rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e fail_with(Failure::Unknown, e.message) From 686b946c5b1fa6e0f653d6c3676275497607c83e Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 13 Dec 2022 16:27:53 -0500 Subject: [PATCH 2/8] Use a new TicketStorage class The goal is to provide an abstraction for how Kerberos tickets are persisted to disk. --- .../kerberos/service_authenticator/base.rb | 62 +++------ .../core/exploit/remote/kerberos/ticket.rb | 30 +---- .../exploit/remote/kerberos/ticket_storage.rb | 127 ++++++++++++++++++ .../auxiliary/admin/kerberos/pkinit_login.rb | 10 +- 4 files changed, 152 insertions(+), 77 deletions(-) create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket_storage.rb diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb index 137ef0ce6fde..7da3a2215f03 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -284,7 +284,7 @@ def build_spn(options = {}) # @param [Hash] options # @option options [Boolean] :use_cached_credentials Override the @use_cached_credentials attribute - # @see #authenticate_via_kdc Options dcoumentation + # @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 = {}) @@ -484,14 +484,13 @@ 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) + ccache = 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}") + Msf::Exploit::Remote::Kerberos::TicketStorage.new(framework_module: @framework_module).store_ccache(ccache, host: rhost) end - 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 @@ -858,15 +857,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( + ccache = 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}") + if options[:credential_cache_username].present? + client = options[:credential_cache_username] + else + client = self.username + end + Msf::Exploit::Remote::Kerberos::TicketStorage.new(framework_module: @framework_module).store_ccache( + ccache, + host: rhost, + client: client, + server: sname + ) end tgs_ticket = tgs_res.ticket @@ -885,21 +891,12 @@ 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 + Msf::Exploit::Remote::Kerberos::TicketStorage.new(framework_module: self).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 @@ -957,19 +954,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 [String] :realm the ticket realm - # @option options [String] :sname the ticket service name - # @option options [String] :username the ticket username - # @return [String] the info string - def loot_info(options = {}) - Msf::Exploit::Remote::Kerberos::Ticket.loot_info( - client: options.fetch(:username) { self.username }, - server: options.fetch(:sname, nil), - realm: options.fetch(:realm) { self.realm } - ) - end end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket.rb b/lib/msf/core/exploit/remote/kerberos/ticket.rb index 8ef76af477b6..1ff8f1939ceb 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket.rb @@ -63,10 +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 - loot_info = Kerberos::Ticket.loot_info(opts) - path = store_loot('mit.kerberos.ccache', 'application/octet-stream', domain, ccache.encode, nil, loot_info) - print_good("MIT Credential Cache ticket saved on #{path}") + Kerberos::TicketStorage.new(framework_module: self).store_ccache(ccache, host: domain) end + ccache end @@ -179,31 +178,6 @@ def print_ccache_contents(ccache, key: nil) presenter = Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(ccache) print_status presenter.present(key: key) end - - # Build a loot info string that can later be used in a lookup. - # - # @param [Hash] options - # @option options [String] :realm the realm of the ticket (optional) - # @option options [String] :server the service name of the ticket (optional) - # @option options [String] :client the client username of the ticket (optional) - # @option options [Boolean] :valid whether or not the ticket is valid, defaults to true (optional) - # @return [String] the info string - def self.loot_info(options = {}) - info = [] - - info << '[invalid]' if !options.fetch(:valid, true) - - realm = options[:realm] - info << "realm: #{realm.to_s.upcase}" if realm.present? - - client = options[:client] - info << "client: #{client.to_s.downcase}" if client.present? - - server = options[:server] - info << "server: #{server.to_s.downcase}" if server.present? - - info.join(', ') - end end end end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket_storage.rb b/lib/msf/core/exploit/remote/kerberos/ticket_storage.rb new file mode 100644 index 000000000000..dcccd7b59a0b --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket_storage.rb @@ -0,0 +1,127 @@ +module Msf::Exploit::Remote::Kerberos + class TicketStorage + extend Forwardable + include Msf::Exploit::Remote::Kerberos::Client + include Msf::Auxiliary::Report + + # @!attribute [r] framework + # @return [Msf::Framework] the Metasploit framework instance + attr_reader :framework + + # @!attribute [r] framework + # @return [Msf::Module] the Metasploit framework module that is associated with the authentication instance + attr_reader :framework_module + + def_delegators :@framework_module, + :print_status, + :print_good, + :vprint_error, + :workspace + + def initialize(framework: nil, framework_module: nil) + @framework = framework || framework_module&.framework + @framework_module = framework_module + end + + def delete_credentials(options = {}) + creds = credentials(options) + framework.db.delete_loot(ids: creds.map(&:id)) + creds + end + + def credentials(options = {}, &block) + objects(options) 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 + block.call(credential) if block_given? + credential + end + end + + def load_credential(options = {}) + return nil unless active_db? + + now = Time.now.utc + credentials(options) do |credential| + 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 + end + + # @param [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] ccache + # @return [nil] + def store_ccache(ccache, options = {}) + realm = options.fetch(:realm) { ccache.default_principal.realm } + # use #components.to_a.join('/') to omit the realm that #to_s includes + client = options.fetch(:client) { ccache.credentials.first&.client&.components.to_a.join('/') } + server = options.fetch(:server) { ccache.credentials.first&.server&.components.to_a.join('/') } + info = loot_info(realm: realm, client: client, server: server) + + path = store_loot('mit.kerberos.ccache', 'application/octet-stream', options[:host], ccache.encode, nil, info) + message = '' + if @framework_module.respond_to?(:peer) && @framework_module.peer.present? && @framework_module.peer != ':' + message << "#{@framework_module.peer} - " + end + if server && server.to_s.downcase.start_with?('krbtgt/') + message << 'TGT ' + else + message << 'TGS ' + end + message << "MIT Credential Cache ticket saved to #{path}" + print_status(message) + + nil + end + + private + + def objects(options, &block) + return [] unless active_db? + + filter = {} + if options[:host].present? + if options[:host].is_a?(Mdm::Host) + filter[:host] = options[:host] + else + filter[:host] = { address: options[:host] } + end + end + unless (info = loot_info(options)).blank? + filter[:info] = info + end + framework.db.loots(workspace: myworkspace, ltype: 'mit.kerberos.ccache', **filter).each do |stored_loot| + block.call(stored_loot) + end + end + + # Build a loot info string that can later be used in a lookup. + # + # @param [Hash] options + # @option options [String] :realm the realm of the ticket (optional) + # @option options [String] :server the service name of the ticket (optional) + # @option options [String] :client the client username of the ticket (optional) + # @option options [Boolean] :valid whether or not the ticket is valid, defaults to true (optional) + # @return [String] the info string + def loot_info(options = {}) + info = [] + + info << '[invalid]' if !options.fetch(:valid, true) + + realm = options[:realm] + info << "realm: #{realm.to_s.upcase}" if realm.present? + + client = options[:client] + info << "client: #{client.to_s.downcase}" if client.present? + + server = options[:server] + info << "server: #{server.to_s.downcase}" if server.present? + + info.join(', ') + end + end +end + diff --git a/modules/auxiliary/admin/kerberos/pkinit_login.rb b/modules/auxiliary/admin/kerberos/pkinit_login.rb index 842e74a4002e..8ee0fa2413a7 100644 --- a/modules/auxiliary/admin/kerberos/pkinit_login.rb +++ b/modules/auxiliary/admin/kerberos/pkinit_login.rb @@ -72,13 +72,6 @@ def run print_good('Successfully authenticated with certificate') enc_part = decrypt_kdc_as_rep_enc_part(tgt_result.as_rep, key) - loot_info = Msf::Exploit::Remote::Kerberos::Ticket.loot_info( - client: username, - server: server_name, - realm: realm, - valid: true - ) - report_service( host: rhost, port: rport, @@ -88,8 +81,7 @@ def run ) ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(tgt_result.as_rep, enc_part) - path = store_loot('mit.kerberos.ccache', 'application/octet-stream', rhost, ccache.encode, nil, loot_info) - print_status("#{peer} - TGT MIT Credential Cache saved to #{path}") + Kerberos::TicketStorage.new(framework_module: self).store_ccache(ccache, host: rhost) rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e fail_with(Failure::Unknown, e.message) rescue ::EOFError, Errno::ECONNRESET, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error => e From b2a4bea76124783dd48b862c3d175653c9265c40 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 14 Dec 2022 11:49:43 -0500 Subject: [PATCH 3/8] Breakout the ticket storage backend drivers --- .../kerberos/service_authenticator/base.rb | 113 ++++++++---------- .../core/exploit/remote/kerberos/ticket.rb | 2 +- .../exploit/remote/kerberos/ticket/storage.rb | 10 ++ .../storage/base.rb} | 53 ++------ .../remote/kerberos/ticket/storage/none.rb | 4 + .../kerberos/ticket/storage/read_mixin.rb | 28 +++++ .../kerberos/ticket/storage/read_only.rb | 5 + .../kerberos/ticket/storage/read_write.rb | 6 + .../kerberos/ticket/storage/write_mixin.rb | 39 ++++++ .../kerberos/ticket/storage/write_only.rb | 5 + .../auxiliary/admin/kerberos/pkinit_login.rb | 2 +- 11 files changed, 159 insertions(+), 108 deletions(-) create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket/storage.rb rename lib/msf/core/exploit/remote/kerberos/{ticket_storage.rb => ticket/storage/base.rb} (55%) create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket/storage/none.rb create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket/storage/read_only.rb create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket/storage/read_write.rb create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket/storage/write_only.rb diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb index 7da3a2215f03..79d3f7bf5441 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -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::Kicket::Storage::Base] the ticket storage driver + attr_reader :ticket_storage # @!attribute [r] key # @return [String] the encryption key for authentication @@ -114,6 +110,7 @@ def initialize( cache_file: nil, use_cached_credentials: true, store_credential_cache: true, + ticket_storage: nil, key: nil, etype: nil ) @@ -130,8 +127,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 @@ -175,7 +174,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] @@ -283,28 +282,25 @@ def build_spn(options = {}) 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 # @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) @@ -312,20 +308,17 @@ def request_tgt_only(options = {}) 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) @@ -338,7 +331,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 = {}) @@ -359,7 +352,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( { @@ -390,7 +383,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 = {}) @@ -429,7 +422,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] } @@ -485,10 +478,7 @@ def authenticate_via_kdc(options = {}) print_good("#{peer} - Received a valid TGT-Response") ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(tgt_result.as_rep, tgt_result.decrypted_part) - - if store_credential_cache - Msf::Exploit::Remote::Kerberos::TicketStorage.new(framework_module: @framework_module).store_ccache(ccache, host: rhost) - end + options.fetch(:ticket_storage, @ticket_storage).store_ccache(ccache, host: rhost) credential = ccache.credentials.first session_key = Rex::Proto::Kerberos::Model::EncryptionKey.new( @@ -579,7 +569,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( @@ -786,7 +776,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] :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 @@ -856,24 +846,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 } - 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 - Msf::Exploit::Remote::Kerberos::TicketStorage.new(framework_module: @framework_module).store_ccache( - ccache, - host: rhost, - client: client, - server: sname - ) + 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( @@ -891,7 +879,8 @@ 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 = {}) - Msf::Exploit::Remote::Kerberos::TicketStorage.new(framework_module: self).load_credential( + driver = options.fetch(:ticket_storage, @ticket_storage) + driver.load_credential( host: rhost, client: options.fetch(:username) { self.username }, server: options.fetch(:sname, nil), diff --git a/lib/msf/core/exploit/remote/kerberos/ticket.rb b/lib/msf/core/exploit/remote/kerberos/ticket.rb index 1ff8f1939ceb..260c8d356f3b 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket.rb @@ -63,7 +63,7 @@ def forge_ticket(enc_key:, enc_type:, start_time:, end_time:, sname:, flags:, ccache = ticket_as_krb5ccache(ticket, opts: opts) if save_ccache - Kerberos::TicketStorage.new(framework_module: self).store_ccache(ccache, host: domain) + Kerberos::Ticket::Storage.store_ccache(ccache, host: domain, framework_module: self) end ccache diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb new file mode 100644 index 000000000000..2d2305da0e89 --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb @@ -0,0 +1,10 @@ +require 'msf/core/exploit/remote/kerberos/ticket/storage/base' + +module Msf::Exploit::Remote::Kerberos::Ticket + module Storage + def self.store_ccache(ccache, options = {}) + driver = WriteOnly.new(framework_module: options[:framework_module]) + driver.store_ccache(ccache, options) + end + end +end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket_storage.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb similarity index 55% rename from lib/msf/core/exploit/remote/kerberos/ticket_storage.rb rename to lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb index dcccd7b59a0b..5e4875e7bc00 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket_storage.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb @@ -1,7 +1,6 @@ -module Msf::Exploit::Remote::Kerberos - class TicketStorage +module Msf::Exploit::Remote::Kerberos::Ticket::Storage + class Base extend Forwardable - include Msf::Exploit::Remote::Kerberos::Client include Msf::Auxiliary::Report # @!attribute [r] framework @@ -15,7 +14,6 @@ class TicketStorage def_delegators :@framework_module, :print_status, :print_good, - :vprint_error, :workspace def initialize(framework: nil, framework_module: nil) @@ -23,62 +21,30 @@ def initialize(framework: nil, framework_module: nil) @framework_module = framework_module end + # return [Array] def delete_credentials(options = {}) - creds = credentials(options) - framework.db.delete_loot(ids: creds.map(&:id)) - creds + [] end + # return [Array] def credentials(options = {}, &block) - objects(options) 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 - block.call(credential) if block_given? - credential - end + [] end + # return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] credential def load_credential(options = {}) - return nil unless active_db? - - now = Time.now.utc - credentials(options) do |credential| - 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 end # @param [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] ccache # @return [nil] def store_ccache(ccache, options = {}) - realm = options.fetch(:realm) { ccache.default_principal.realm } - # use #components.to_a.join('/') to omit the realm that #to_s includes - client = options.fetch(:client) { ccache.credentials.first&.client&.components.to_a.join('/') } - server = options.fetch(:server) { ccache.credentials.first&.server&.components.to_a.join('/') } - info = loot_info(realm: realm, client: client, server: server) - - path = store_loot('mit.kerberos.ccache', 'application/octet-stream', options[:host], ccache.encode, nil, info) - message = '' - if @framework_module.respond_to?(:peer) && @framework_module.peer.present? && @framework_module.peer != ':' - message << "#{@framework_module.peer} - " - end - if server && server.to_s.downcase.start_with?('krbtgt/') - message << 'TGT ' - else - message << 'TGS ' - end - message << "MIT Credential Cache ticket saved to #{path}" - print_status(message) - nil end private + # return [Array] def objects(options, &block) return [] unless active_db? @@ -94,7 +60,7 @@ def objects(options, &block) filter[:info] = info end framework.db.loots(workspace: myworkspace, ltype: 'mit.kerberos.ccache', **filter).each do |stored_loot| - block.call(stored_loot) + block.call(stored_loot) if block_given? end end @@ -124,4 +90,3 @@ def loot_info(options = {}) end end end - diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/none.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/none.rb new file mode 100644 index 000000000000..7cf5227c2965 --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/none.rb @@ -0,0 +1,4 @@ +module Msf::Exploit::Remote::Kerberos::Ticket::Storage + class None < Base + end +end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb new file mode 100644 index 000000000000..6c20bb8c1792 --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb @@ -0,0 +1,28 @@ +module Msf::Exploit::Remote::Kerberos::Ticket::Storage + module ReadMixin + # return [Array] + def credentials(options = {}, &block) + objects(options) 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 + block.call(credential) if block_given? + credential + end + end + + # return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] credential + def load_credential(options = {}) + return nil unless active_db? + + now = Time.now.utc + credentials(options) do |credential| + 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 + end + end +end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_only.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_only.rb new file mode 100644 index 000000000000..579af3b47ff2 --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_only.rb @@ -0,0 +1,5 @@ +module Msf::Exploit::Remote::Kerberos::Ticket::Storage + class ReadOnly < Base + include ReadMixin + end +end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_write.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_write.rb new file mode 100644 index 000000000000..10aa6f08200a --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_write.rb @@ -0,0 +1,6 @@ +module Msf::Exploit::Remote::Kerberos::Ticket::Storage + class ReadWrite < Base + include ReadMixin + include WriteMixin + end +end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb new file mode 100644 index 000000000000..b8cbdd7217ba --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb @@ -0,0 +1,39 @@ +module Msf::Exploit::Remote::Kerberos::Ticket::Storage + module WriteMixin + # return [Array] + def delete_credentials(options = {}) + objects = objects(options) + framework.db.delete_loot(ids: objects.map(&:id)) + objects.map 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 + ccache.credentials.first + end + end + + # @param [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] ccache + # @return [nil] + def store_ccache(ccache, options = {}) + realm = options.fetch(:realm) { ccache.default_principal.realm } + # use #components.to_a.join('/') to omit the realm that #to_s includes + client = options.fetch(:client) { ccache.credentials.first&.client&.components.to_a.join('/') } + server = options.fetch(:server) { ccache.credentials.first&.server&.components.to_a.join('/') } + info = loot_info(realm: realm, client: client, server: server) + + path = store_loot('mit.kerberos.ccache', 'application/octet-stream', options[:host], ccache.encode, nil, info) + message = '' + if @framework_module.respond_to?(:peer) && @framework_module.peer.present? && @framework_module.peer != ':' + message << "#{@framework_module.peer} - " + end + if server && server.to_s.downcase.start_with?('krbtgt/') + message << 'TGT ' + else + message << 'TGS ' + end + message << "MIT Credential Cache ticket saved to #{path}" + print_status(message) + + nil + end + end +end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_only.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_only.rb new file mode 100644 index 000000000000..c8720a1636bd --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_only.rb @@ -0,0 +1,5 @@ +module Msf::Exploit::Remote::Kerberos::Ticket::Storage + class WriteOnly < Base + include WriteMixin + end +end diff --git a/modules/auxiliary/admin/kerberos/pkinit_login.rb b/modules/auxiliary/admin/kerberos/pkinit_login.rb index 8ee0fa2413a7..48efae073dba 100644 --- a/modules/auxiliary/admin/kerberos/pkinit_login.rb +++ b/modules/auxiliary/admin/kerberos/pkinit_login.rb @@ -81,7 +81,7 @@ def run ) ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(tgt_result.as_rep, enc_part) - Kerberos::TicketStorage.new(framework_module: self).store_ccache(ccache, host: rhost) + Kerberos::Ticket::Storage.store_ccache(ccache, host: rhost, framework_module: self) rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e fail_with(Failure::Unknown, e.message) rescue ::EOFError, Errno::ECONNRESET, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error => e From fea259f6e7032b1695823cc682097c6a6297c19b Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 14 Dec 2022 12:59:27 -0500 Subject: [PATCH 4/8] Switch everything to use the ticket storage --- lib/metasploit/framework/mssql/client.rb | 3 +- .../kerberos/service_authenticator/base.rb | 2 - .../core/exploit/remote/kerberos/ticket.rb | 2 +- .../exploit/remote/kerberos/ticket/storage.rb | 49 +++++++++++++++++++ lib/msf/core/exploit/remote/ldap.rb | 5 +- lib/msf/core/exploit/remote/mssql.rb | 4 +- lib/msf/core/exploit/remote/smb/client.rb | 3 +- .../remote/smb/client/authenticated.rb | 5 +- .../auxiliary/admin/kerberos/get_ticket.rb | 27 +++------- modules/auxiliary/scanner/smb/smb_login.rb | 3 +- modules/auxiliary/scanner/winrm/winrm_cmd.rb | 4 +- .../auxiliary/scanner/winrm/winrm_login.rb | 4 +- 12 files changed, 77 insertions(+), 34 deletions(-) diff --git a/lib/metasploit/framework/mssql/client.rb b/lib/metasploit/framework/mssql/client.rb index 7a9c6f120f91..e9864cfc99e3 100644 --- a/lib/metasploit/framework/mssql/client.rb +++ b/lib/metasploit/framework/mssql/client.rb @@ -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 diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb index 79d3f7bf5441..28b2f974f44d 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -108,8 +108,6 @@ 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 diff --git a/lib/msf/core/exploit/remote/kerberos/ticket.rb b/lib/msf/core/exploit/remote/kerberos/ticket.rb index 260c8d356f3b..bdb4849671f0 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket.rb @@ -63,7 +63,7 @@ def forge_ticket(enc_key:, enc_type:, start_time:, end_time:, sname:, flags:, ccache = ticket_as_krb5ccache(ticket, opts: opts) if save_ccache - Kerberos::Ticket::Storage.store_ccache(ccache, host: domain, framework_module: self) + Kerberos::Ticket::Storage.store_ccache(ccache, framework_module: self) end ccache diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb index 2d2305da0e89..d84d0362a826 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb @@ -6,5 +6,54 @@ 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 mode', + 'read-write', + %w[ none read-only write-only read-write ] + ] + ) + ] + ) + end + + # @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 diff --git a/lib/msf/core/exploit/remote/ldap.rb b/lib/msf/core/exploit/remote/ldap.rb index 84c76c69aa09..92fc43fed431 100644 --- a/lib/msf/core/exploit/remote/ldap.rb +++ b/lib/msf/core/exploit/remote/ldap.rb @@ -8,6 +8,8 @@ module Msf module Exploit::Remote::LDAP + include Msf::Exploit::Remote::Kerberos::Ticket::Storage + def initialize(info = {}) super @@ -71,7 +73,8 @@ def get_connect_opts password: datastore['PASSWORD'], framework: framework, framework_module: self, - cache_file: datastore['LdapKrb5Ccname'].blank? ? nil : datastore['LdapKrb5Ccname'] + cache_file: datastore['LdapKrb5Ccname'].blank? ? nil : datastore['LdapKrb5Ccname'], + ticket_storage: kerberos_ticket_storage ) kerberos_result = kerberos_authenticator.authenticate diff --git a/lib/msf/core/exploit/remote/mssql.rb b/lib/msf/core/exploit/remote/mssql.rb index 0473855a3ab9..ddf5c67638fa 100644 --- a/lib/msf/core/exploit/remote/mssql.rb +++ b/lib/msf/core/exploit/remote/mssql.rb @@ -16,6 +16,7 @@ module Exploit::Remote::MSSQL include Exploit::Remote::Tcp include Exploit::Remote::NTLM::Client include Metasploit::Framework::MSSQL::Base + include Exploit::Remote::Kerberos::Ticket::Storage # # Creates an instance of a MSSQL exploit module. @@ -358,7 +359,8 @@ def mssql_login(user='sa', pass='', db='') password: datastore['password'], framework: framework, framework_module: self, - cache_file: datastore['MssqlKrb5Ccname'].blank? ? nil : datastore['MssqlKrb5Ccname'] + cache_file: datastore['MssqlKrb5Ccname'].blank? ? nil : datastore['MssqlKrb5Ccname'], + ticket_storage: kerberos_ticket_storage ) kerberos_result = kerberos_authenticator.authenticate diff --git a/lib/msf/core/exploit/remote/smb/client.rb b/lib/msf/core/exploit/remote/smb/client.rb index b98b84b78aa2..c41a2a098de2 100644 --- a/lib/msf/core/exploit/remote/smb/client.rb +++ b/lib/msf/core/exploit/remote/smb/client.rb @@ -163,7 +163,8 @@ def smb_login(simple_client = self.simple) password: datastore['SMBPass'], framework: framework, framework_module: self, - cache_file: datastore['SmbKrb5Ccname'].blank? ? nil : datastore['SmbKrb5Ccname'] + cache_file: datastore['SmbKrb5Ccname'].blank? ? nil : datastore['SmbKrb5Ccname'], + ticket_storage: kerberos_ticket_storage ) simple_client.client.extend(Msf::Exploit::Remote::SMB::Client::KerberosAuthentication) diff --git a/lib/msf/core/exploit/remote/smb/client/authenticated.rb b/lib/msf/core/exploit/remote/smb/client/authenticated.rb index 4e52fa4b22dc..16628d7d5d36 100644 --- a/lib/msf/core/exploit/remote/smb/client/authenticated.rb +++ b/lib/msf/core/exploit/remote/smb/client/authenticated.rb @@ -7,6 +7,7 @@ module Msf module Exploit::Remote::SMB::Client::Authenticated include Msf::Exploit::Remote::SMB::Client + include Msf::Exploit::Remote::Kerberos::Ticket::Storage def initialize(info = {}) super @@ -22,9 +23,7 @@ def initialize(info = {}) OptEnum.new('SMBAuth', [true, 'The Authentication mechanism to use', Msf::Exploit::Remote::AuthOption::AUTO, Msf::Exploit::Remote::AuthOption::SMB_OPTIONS]), OptString.new('SmbRhostname', [false, 'The rhostname which is required for kerberos']), OptAddress.new('DomainControllerRhost', [false, 'The resolvable rhost for the Domain Controller']), - OptPath.new('SmbKrb5Ccname', [false, 'The ccache file to use for kerberos authentication', ENV.fetch('SMBKRB5CCNAME', ENV.fetch('KRB5CCNAME', nil))], conditions: %w[ SMBAuth == kerberos ]), - OptBool.new('KrbUseCachedCredentials', [false, 'Use credentials stored in the database for kerberos authentication', false], conditions: %w[ SMBAuth == kerberos ]), - OptBool.new('KrbStoreCredentialCache', [false, 'Store Kerberos TGS MIT Credential Cache to the database if authentication succeed', true], conditions: %w[ SMBAuth == kerberos ]) + OptPath.new('SmbKrb5Ccname', [false, 'The ccache file to use for kerberos authentication', ENV.fetch('SMBKRB5CCNAME', ENV.fetch('KRB5CCNAME', nil))], conditions: %w[ SMBAuth == kerberos ]) ], Msf::Exploit::Remote::SMB::Client::Authenticated ) diff --git a/modules/auxiliary/admin/kerberos/get_ticket.rb b/modules/auxiliary/admin/kerberos/get_ticket.rb index 3eafac4ca4e2..5c1e718fca17 100644 --- a/modules/auxiliary/admin/kerberos/get_ticket.rb +++ b/modules/auxiliary/admin/kerberos/get_ticket.rb @@ -5,7 +5,9 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report + include Msf::Exploit::Remote::Kerberos include Msf::Exploit::Remote::Kerberos::Client + include Msf::Exploit::Remote::Kerberos::Ticket::Storage def initialize(info = {}) super( @@ -65,19 +67,6 @@ def initialize(info = {}) ) ] ) - - register_advanced_options( - [ - OptBool.new( - 'KrbUseCachedCredentials', [ - true, - 'Use credentials stored in the database for Kerberos authentication when requesting a TGS', - true - ], - conditions: %w[ACTION == GET_TGS] - ), - ] - ) end def validate_options @@ -127,7 +116,8 @@ def init_authenticator(options = {}) realm: datastore['DOMAIN'], username: datastore['USER'], framework: framework, - framework_module: self + framework_module: self, + ticket_storage: kerberos_ticket_storage }) options[:password] = datastore['PASSWORD'] if datastore['PASSWORD'].present? if datastore['NTHASH'].present? @@ -149,14 +139,11 @@ def init_authenticator(options = {}) def action_get_tgt print_status("#{peer} - Getting TGT for #{datastore['USER']}@#{datastore['DOMAIN']}") - authenticator = init_authenticator({ use_cached_credentials: false }) + authenticator = init_authenticator({ ticket_storage: kerberos_ticket_storage(read: false) }) authenticator.request_tgt_only end def action_get_tgs - options = { - use_cached_credentials: datastore['KrbUseCachedCredentials'].nil? ? false : datastore['KrbUseCachedCredentials'] - } authenticator = init_authenticator(options) credential = authenticator.request_tgt_only(options) @@ -173,7 +160,7 @@ def action_get_tgs } tgs_ticket, _tgs_auth = authenticator.s4u2self( credential, - auth_options.merge(store_credential_cache: false) + auth_options.merge(ticket_storage: kerberos_ticket_storage(read: false)) ) auth_options[:sname] = Rex::Proto::Kerberos::Model::PrincipalName.new( @@ -191,7 +178,7 @@ def action_get_tgs ) tgs_options = { sname: sname, - use_cached_credentials: false + ticket_storage: kerberos_ticket_storage(read: false) } authenticator.request_tgs_only(credential, tgs_options) end diff --git a/modules/auxiliary/scanner/smb/smb_login.rb b/modules/auxiliary/scanner/smb/smb_login.rb index 1bbaeed8b427..ffc8e3f4c7df 100644 --- a/modules/auxiliary/scanner/smb/smb_login.rb +++ b/modules/auxiliary/scanner/smb/smb_login.rb @@ -87,8 +87,7 @@ def run_host(ip) framework: framework, framework_module: self, cache_file: datastore['SmbKrb5Ccname'].blank? ? nil : datastore['SmbKrb5Ccname'], - use_cached_credentials: datastore['KrbUseCachedCredentials'].nil? ? false : datastore['KrbUseCachedCredentials'], - store_credential_cache: datastore['KrbStoreCredentialCache'].nil? ? true : datastore['KrbStoreCredentialCache'] + ticket_storage: kerberos_ticket_storage ) end end diff --git a/modules/auxiliary/scanner/winrm/winrm_cmd.rb b/modules/auxiliary/scanner/winrm/winrm_cmd.rb index 2a9bcce454dc..ed4e01b96470 100644 --- a/modules/auxiliary/scanner/winrm/winrm_cmd.rb +++ b/modules/auxiliary/scanner/winrm/winrm_cmd.rb @@ -10,6 +10,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Auxiliary::Scanner include Msf::Exploit::Remote::AuthOption + include Msf::Exploit::Remote::Kerberos::Ticket::Storage def initialize super( @@ -82,7 +83,8 @@ def run_host(ip) framework_module: self, cache_file: datastore['WinrmKrb5Ccname'].blank? ? nil : datastore['WinrmKrb5Ccname'], mutual_auth: true, - use_gss_checksum: true + use_gss_checksum: true, + ticket_storage: kerberos_ticket_storage ) opts = opts.merge({ user: '', # Need to provide it, otherwise the WinRM module complains diff --git a/modules/auxiliary/scanner/winrm/winrm_login.rb b/modules/auxiliary/scanner/winrm/winrm_login.rb index a113a431cba3..18a9ef829363 100644 --- a/modules/auxiliary/scanner/winrm/winrm_login.rb +++ b/modules/auxiliary/scanner/winrm/winrm_login.rb @@ -15,6 +15,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::CommandShell include Msf::Auxiliary::Scanner include Msf::Exploit::Remote::AuthOption + include Msf::Exploit::Remote::Kerberos::Ticket::Storage def initialize super( @@ -75,7 +76,8 @@ def run_host(ip) framework_module: self, cache_file: datastore['WinrmKrb5Ccname'].blank? ? nil : datastore['WinrmKrb5Ccname'], mutual_auth: true, - use_gss_checksum: true + use_gss_checksum: true, + ticket_storage: kerberos_ticket_storage ) end end From 663dee982e6ed670b9b1f225ba0f797e558348f7 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 14 Dec 2022 13:27:43 -0500 Subject: [PATCH 5/8] Expose an abstract stored ticket object --- .../exploit/remote/kerberos/ticket/storage.rb | 2 +- .../remote/kerberos/ticket/storage/base.rb | 10 +-- .../kerberos/ticket/storage/read_mixin.rb | 28 ++++----- .../kerberos/ticket/storage/stored_ticket.rb | 63 +++++++++++++++++++ .../kerberos/ticket/storage/write_mixin.rb | 9 +-- 5 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 lib/msf/core/exploit/remote/kerberos/ticket/storage/stored_ticket.rb diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb index d84d0362a826..b0783e277cf6 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb @@ -14,7 +14,7 @@ def initialize(info = {}) Msf::OptEnum.new( 'KrbCacheMode', [ true, - 'Kerberos ticket cache mode', + 'Kerberos ticket cache storage mode', 'read-write', %w[ none read-only write-only read-write ] ] diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb index 5e4875e7bc00..416ab6b1794c 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb @@ -21,16 +21,18 @@ def initialize(framework: nil, framework_module: nil) @framework_module = framework_module end - # return [Array] - def delete_credentials(options = {}) + # return [Array] + def delete_tickets(options = {}) [] end - # return [Array] - def credentials(options = {}, &block) + # return [Array] + def tickets(options = {}, &block) [] end + # Load a stored credential object that is suitable for authenticaiton. + # # return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] credential def load_credential(options = {}) nil diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb index 6c20bb8c1792..a0819d9a7d8e 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb @@ -1,28 +1,24 @@ module Msf::Exploit::Remote::Kerberos::Ticket::Storage module ReadMixin - # return [Array] - def credentials(options = {}, &block) - objects(options) 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 - block.call(credential) if block_given? - credential - end - end - - # return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] credential def load_credential(options = {}) return nil unless active_db? now = Time.now.utc - credentials(options) do |credential| - 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 + tickets(options) do |ticket| + next if ticket.expired?(now) + + return ticket.ccache.credentials.first end nil end + + def tickets(options = {}, &block) + objects(options).map do |stored_loot| + stored_ticket = StoredTicket.new(stored_loot) + block.call(stored_ticket) if block_given? + stored_ticket + end + end end end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/stored_ticket.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/stored_ticket.rb new file mode 100644 index 000000000000..59263387a920 --- /dev/null +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/stored_ticket.rb @@ -0,0 +1,63 @@ +module Msf::Exploit::Remote::Kerberos::Ticket::Storage + # A StoredTicket object that internally holds a TGT/TGS object. This class abstracts the underlying persistence + # implementation, as currently this data is stored as {Mdm::Loot} - but in the future may be migrated to a + # {Metasploit::Credential::Login} or similar in the future. + class StoredTicket + # @param [Mdm::Loot] loot + def initialize(loot) + @loot = loot + end + + def id + @loot.id + end + + # @return [String] the host address + def host_address + loot.host && loot.host.address ? loot.host.address : '' + end + + def path + loot.path + end + + def principal + credential.client + end + + def sname + credential.server + end + + def starttime + credential.starttime + end + + # @return [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] + def ccache + @ccache ||= Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(loot.data) + end + + # @return [String] human readable info about the ticket + def info + loot.info + end + + # @return [TrueClass, FalseClass] True if the ticket is valid within the starttime/authtime/endtime, false otherwise + def expired?(now = Time.now) + tkt_start = credential.starttime == Time.at(0).utc ? credential.authtime : credential.starttime + tkt_end = credential.endtime + !(tkt_start < now && now < tkt_end) + end + + private + + # @return [Mdm::Loot] loot + attr_reader :loot + + def credential + # at this time Metasploit stores 1 credential per ccache file, so no need to iterate through them + ccache.credentials.first + end + end +end diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb index b8cbdd7217ba..28b52fa71e04 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb @@ -1,18 +1,13 @@ module Msf::Exploit::Remote::Kerberos::Ticket::Storage module WriteMixin - # return [Array] - def delete_credentials(options = {}) + def delete_tickets(options = {}) objects = objects(options) framework.db.delete_loot(ids: objects.map(&:id)) objects.map 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 - ccache.credentials.first + StoredTicket.new(stored_loot) end end - # @param [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] ccache - # @return [nil] def store_ccache(ccache, options = {}) realm = options.fetch(:realm) { ccache.default_principal.realm } # use #components.to_a.join('/') to omit the realm that #to_s includes From 830e850160d24719d5cf66707ad0d724d2bfd02f Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 15 Dec 2022 09:28:59 -0500 Subject: [PATCH 6/8] Add more docs --- .../kerberos/service_authenticator/base.rb | 4 +- .../exploit/remote/kerberos/ticket/storage.rb | 6 +++ .../remote/kerberos/ticket/storage/base.rb | 44 +++++++++++++------ .../kerberos/ticket/storage/read_mixin.rb | 3 ++ .../kerberos/ticket/storage/write_mixin.rb | 3 ++ 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb index 28b2f974f44d..6d90745727e3 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -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 @@ -62,7 +62,7 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base attr_reader :send_delegated_creds # @!attribute [r] ticket_storage - # @return [Msf::Exploit::Remote::Kerberos::Kicket::Storage::Base] the ticket storage driver + # @return [Msf::Exploit::Remote::Kerberos::Ticket::Storage::Base] the ticket storage driver attr_reader :ticket_storage # @!attribute [r] key diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb index b0783e277cf6..baf4270a9fe6 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage.rb @@ -2,6 +2,10 @@ 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) @@ -23,6 +27,8 @@ def initialize(info = {}) ) 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. diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb index 416ab6b1794c..62cec129df89 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb @@ -7,7 +7,7 @@ class 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 @@ -21,24 +21,43 @@ def initialize(framework: nil, framework_module: nil) @framework_module = framework_module end - # return [Array] + # Delete tickets matching the options query. + # + # @param [Hash] options See the options hash description in {#tickets}. + # @return [Array] def delete_tickets(options = {}) [] end - # return [Array] + # Get stored tickets matching the options query. + # + # @param [Hash] options The options for matching tickets. The :realm, :server, :client and :status options are all + # processed as a group. If any one or more of them are specified, they are all used for filtering. It can not for + # example specify client and fetch all tickets for a particular client where the server is different. + # @option options [String] :host The host for the ticket (optional) + # @option options [String] :realm The realm of the ticket (optional) + # @option options [String] :server The service name of the ticket (optional) + # @option options [String] :client The client username of the ticket (optional) + # @option options [Symbol] :status The ticket status, defaults to valid (optional) + # @return [Array] def tickets(options = {}, &block) [] end - # Load a stored credential object that is suitable for authenticaiton. + # Load a stored credential object that is suitable for authentication. # - # return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] credential + # @param [Hash] options See the options description in #tickets. + # @return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential, nil] credential The credential if one was + # found. def load_credential(options = {}) nil end - # @param [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] ccache + # Store the specified object. + # + # @param [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] ccache The credential cache object to store. + # @param [Hash] options The information associated with the stored object. See the options hash description in + # {#tickets}. # @return [nil] def store_ccache(ccache, options = {}) nil @@ -46,7 +65,10 @@ def store_ccache(ccache, options = {}) private - # return [Array] + # Return the raw stored objects. + # + # @param [Hash] options See the options hash description in {#tickets}. + # @return [Array] def objects(options, &block) return [] unless active_db? @@ -68,16 +90,12 @@ def objects(options, &block) # Build a loot info string that can later be used in a lookup. # - # @param [Hash] options - # @option options [String] :realm the realm of the ticket (optional) - # @option options [String] :server the service name of the ticket (optional) - # @option options [String] :client the client username of the ticket (optional) - # @option options [Boolean] :valid whether or not the ticket is valid, defaults to true (optional) # @return [String] the info string def loot_info(options = {}) info = [] - info << '[invalid]' if !options.fetch(:valid, true) + status = options.fetch(:status, :valid).downcase.to_sym + info << "status: #{status}" unless status == :valid realm = options[:realm] info << "realm: #{realm.to_s.upcase}" if realm.present? diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb index a0819d9a7d8e..bf07dff9ef7f 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/read_mixin.rb @@ -1,5 +1,7 @@ module Msf::Exploit::Remote::Kerberos::Ticket::Storage + # A mixin providing the ability to read previously stored tickets. module ReadMixin + # (see Base#load_credential) def load_credential(options = {}) return nil unless active_db? @@ -13,6 +15,7 @@ def load_credential(options = {}) nil end + # (see Base#tickets) def tickets(options = {}, &block) objects(options).map do |stored_loot| stored_ticket = StoredTicket.new(stored_loot) diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb index 28b52fa71e04..b20ea8f42cef 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb @@ -1,5 +1,7 @@ module Msf::Exploit::Remote::Kerberos::Ticket::Storage + # A mixin providing the ability to store new and delete existing tickets. module WriteMixin + # (see Base#delete_tickets) def delete_tickets(options = {}) objects = objects(options) framework.db.delete_loot(ids: objects.map(&:id)) @@ -8,6 +10,7 @@ def delete_tickets(options = {}) end end + # (see Base#store_ccache) def store_ccache(ccache, options = {}) realm = options.fetch(:realm) { ccache.default_principal.realm } # use #components.to_a.join('/') to omit the realm that #to_s includes From 75fc560d195c0cfc43eb017ff75c6b5ef73b2182 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 15 Dec 2022 17:40:17 -0500 Subject: [PATCH 7/8] Handle cases where the framework module is nil --- .../remote/kerberos/ticket/storage/base.rb | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb index 62cec129df89..02c2c6d568c8 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb @@ -11,11 +11,6 @@ class Base # @return [Msf::Module] the Metasploit framework module that is associated with the authentication instance attr_reader :framework_module - def_delegators :@framework_module, - :print_status, - :print_good, - :workspace - def initialize(framework: nil, framework_module: nil) @framework = framework || framework_module&.framework @framework_module = framework_module @@ -63,8 +58,28 @@ def store_ccache(ccache, options = {}) nil 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 + private + # Forward whatever method calls this to the specified framework module, but + # only if it's present otherwise return nil. + def proxied_module_method(*args, **kwargs) + return nil unless @framework_module + + @framework_module.send(__callee__, *args, **kwargs) + end + + alias :print_good :proxied_module_method + alias :print_status :proxied_module_method + # Return the raw stored objects. # # @param [Hash] options See the options hash description in {#tickets}. From 60a76da374d959925f27acefbb592886886d2c5c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 15 Dec 2022 18:02:47 -0500 Subject: [PATCH 8/8] Allow deleting tickets by ID --- .../exploit/remote/kerberos/ticket/storage/base.rb | 3 +++ .../remote/kerberos/ticket/storage/write_mixin.rb | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb index 02c2c6d568c8..a89a5fd81ff7 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/base.rb @@ -19,6 +19,7 @@ def initialize(framework: nil, framework_module: nil) # Delete tickets matching the options query. # # @param [Hash] options See the options hash description in {#tickets}. + # @option options [Array] :ids The identifiers of the tickets to delete (optional) # @return [Array] def delete_tickets(options = {}) [] @@ -29,6 +30,7 @@ def delete_tickets(options = {}) # @param [Hash] options The options for matching tickets. The :realm, :server, :client and :status options are all # processed as a group. If any one or more of them are specified, they are all used for filtering. It can not for # example specify client and fetch all tickets for a particular client where the server is different. + # @option options [Integer, Array] :id The identifier of the ticket (optional) # @option options [String] :host The host for the ticket (optional) # @option options [String] :realm The realm of the ticket (optional) # @option options [String] :server The service name of the ticket (optional) @@ -88,6 +90,7 @@ def objects(options, &block) return [] unless active_db? filter = {} + filter[:id] = options[:id] if options[:id].present? if options[:host].present? if options[:host].is_a?(Mdm::Host) filter[:host] = options[:host] diff --git a/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb index b20ea8f42cef..6fd9dc6e851b 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket/storage/write_mixin.rb @@ -3,9 +3,14 @@ module Msf::Exploit::Remote::Kerberos::Ticket::Storage module WriteMixin # (see Base#delete_tickets) def delete_tickets(options = {}) - objects = objects(options) - framework.db.delete_loot(ids: objects.map(&:id)) - objects.map do |stored_loot| + if options.keys == [:ids] + # skip calling #objects which issues a query when the IDs are specified + ids = options[:ids] + else + ids = objects(options).map(&:id) + end + + framework.db.delete_loot(ids: ids).map do |stored_loot| StoredTicket.new(stored_loot) end end