From 1885b650ba5382df8df11f6de5fcb1af59f2c240 Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Wed, 29 Jan 2025 11:10:30 +0100 Subject: [PATCH] Fix ldap_login and smb_login --- .../framework/credential_collection.rb | 29 +++++++++-- modules/auxiliary/scanner/ldap/ldap_login.rb | 21 ++++++-- modules/auxiliary/scanner/smb/smb_login.rb | 18 +++++-- .../framework/credential_collection_spec.rb | 49 ++++++++++++++++++- 4 files changed, 106 insertions(+), 11 deletions(-) diff --git a/lib/metasploit/framework/credential_collection.rb b/lib/metasploit/framework/credential_collection.rb index 57f1f7726cfb..11e8a4f81867 100644 --- a/lib/metasploit/framework/credential_collection.rb +++ b/lib/metasploit/framework/credential_collection.rb @@ -212,6 +212,23 @@ class CredentialCollection < PrivateCredentialCollection # @return [Boolean] attr_accessor :anonymous_login + # @!attribute ignore_private + # Whether to ignore private (password). This is usually set when Kerberos + # or Schannel authentication is requested and the credentials are + # retrieved from cache or from a file. This attribute should be true in + # these scenarios, otherwise validation will fail since the password is not + # provided. + # @return [Boolean] + attr_accessor :ignore_private + + # @!attribute ignore_public + # Whether to ignore public (username). This is usually set when Schannel + # authentication is requested and the credentials are retrieved from a + # file (certificate). This attribute should be true in this case, + # otherwise validation will fail since the password is not provided. + # @return [Boolean] + attr_accessor :ignore_public + # @option opts [Boolean] :blank_passwords See {#blank_passwords} # @option opts [String] :pass_file See {#pass_file} # @option opts [String] :password See {#password} @@ -240,7 +257,13 @@ def add_public(public_str='') # @yieldparam credential [Metasploit::Framework::Credential] # @return [void] def each_filtered - if password_spray + if ignore_private + if ignore_public + yield Metasploit::Framework::Credential.new(public: nil, private: nil, realm: realm) + else + yield Metasploit::Framework::Credential.new(public: username, private: nil, realm: realm) + end + elsif password_spray each_unfiltered_password_first do |credential| next unless self.filter.nil? || self.filter.call(credential) @@ -510,14 +533,14 @@ def empty? # # @return [Boolean] def has_users? - username.present? || user_file.present? || userpass_file.present? || !additional_publics.empty? + username.present? || user_file.present? || userpass_file.present? || !additional_publics.empty? || !!ignore_public end # Returns true when there are any private values set # # @return [Boolean] def has_privates? - super || userpass_file.present? || user_as_pass + super || userpass_file.present? || user_as_pass || !!ignore_private end end diff --git a/modules/auxiliary/scanner/ldap/ldap_login.rb b/modules/auxiliary/scanner/ldap/ldap_login.rb index 7c5efbabf439..47c6dac14db9 100644 --- a/modules/auxiliary/scanner/ldap/ldap_login.rb +++ b/modules/auxiliary/scanner/ldap/ldap_login.rb @@ -89,12 +89,19 @@ def validate_connect_options! end def run_host(ip) + ignore_public = datastore['LDAP::Auth'] == Msf::Exploit::Remote::AuthOption::SCHANNEL + ignore_private = + datastore['LDAP::Auth'] == Msf::Exploit::Remote::AuthOption::SCHANNEL || + (Msf::Exploit::Remote::AuthOption::KERBEROS && !datastore['ANONYMOUS_LOGIN'] && !datastore['PASSWORD']) + cred_collection = build_credential_collection( username: datastore['USERNAME'], password: datastore['PASSWORD'], realm: datastore['DOMAIN'], anonymous_login: datastore['ANONYMOUS_LOGIN'], - blank_passwords: false + blank_passwords: false, + ignore_public: ignore_public, + ignore_private: ignore_private ) opts = { @@ -107,14 +114,20 @@ def run_host(ip) ldap_cert_file: datastore['LDAP::CertFile'], ldap_rhostname: datastore['Ldap::Rhostname'], ldap_krb_offered_enc_types: datastore['Ldap::KrbOfferedEncryptionTypes'], - ldap_krb5_cname: datastore['Ldap::Krb5Ccname'], - # Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module - kerberos_ticket_storage: kerberos_ticket_storage({ read: false, write: true }) + ldap_krb5_cname: datastore['Ldap::Krb5Ccname'] } realm_key = nil if opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::KERBEROS realm_key = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN + if !datastore['ANONYMOUS_LOGIN'] && !datastore['PASSWORD'] + # In case no password has been provided, we assume the user wants to use Kerberos tickets stored in cache + # Write mode is still enable in case new TGS tickets are retrieved. + opts[:kerberos_ticket_storage] = kerberos_ticket_storage({ read: true, write: true }) + else + # Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module + opts[:kerberos_ticket_storage] = kerberos_ticket_storage({ read: false, write: true }) + end end scanner = Metasploit::Framework::LoginScanner::LDAP.new( diff --git a/modules/auxiliary/scanner/smb/smb_login.rb b/modules/auxiliary/scanner/smb/smb_login.rb index 0519b29657f9..621557d0b850 100644 --- a/modules/auxiliary/scanner/smb/smb_login.rb +++ b/modules/auxiliary/scanner/smb/smb_login.rb @@ -116,6 +116,15 @@ def run_host(ip) fail_with(Msf::Exploit::Failure::BadConfig, 'The SMBDomain option is required when using Kerberos authentication.') if datastore['SMBDomain'].blank? fail_with(Msf::Exploit::Failure::BadConfig, 'The DomainControllerRhost is required when using Kerberos authentication.') if datastore['DomainControllerRhost'].blank? + if !datastore['PASSWORD'] + # In case no password has been provided, we assume the user wants to use Kerberos tickets stored in cache + # Write mode is still enable in case new TGS tickets are retrieved. + ticket_storage = kerberos_ticket_storage({ read: true, write: true }) + else + # Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module + ticket_storage = kerberos_ticket_storage({ read: false, write: true }) + end + kerberos_authenticator_factory = lambda do |username, password, realm| Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::SMB.new( host: datastore['DomainControllerRhost'], @@ -127,8 +136,7 @@ def run_host(ip) framework: framework, framework_module: self, cache_file: datastore['Smb::Krb5Ccname'].blank? ? nil : datastore['Smb::Krb5Ccname'], - # Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module - ticket_storage: kerberos_ticket_storage({ read: false, write: true }) + ticket_storage: ticket_storage ) end end @@ -170,7 +178,8 @@ def run_host(ip) cred_collection = build_credential_collection( realm: domain, username: datastore['SMBUser'], - password: datastore['SMBPass'] + password: datastore['SMBPass'], + ignore_private: datastore['SMB::Auth'] == Msf::Exploit::Remote::AuthOption::KERBEROS && !datastore['PASSWORD'] ) cred_collection = prepend_db_hashes(cred_collection) @@ -256,6 +265,9 @@ def accepts_bogus_domains?(user, pass) end def report_creds(ip, port, result) + # Private can be nil if we authenticated with Kerberos and a cached ticket was used. No need to report this. + return unless result.credential.private + if !datastore['RECORD_GUEST'] && (result.access_level == Metasploit::Framework::LoginScanner::SMB::AccessLevels::GUEST) return end diff --git a/spec/lib/metasploit/framework/credential_collection_spec.rb b/spec/lib/metasploit/framework/credential_collection_spec.rb index 792020ac5eb0..3fd679966d6f 100644 --- a/spec/lib/metasploit/framework/credential_collection_spec.rb +++ b/spec/lib/metasploit/framework/credential_collection_spec.rb @@ -16,7 +16,9 @@ prepended_creds: prepended_creds, additional_privates: additional_privates, additional_publics: additional_publics, - password_spray: password_spray + password_spray: password_spray, + ignore_public: ignore_public, + ignore_private: ignore_private ) end @@ -39,6 +41,8 @@ let(:additional_privates) { [] } let(:additional_publics) { [] } let(:password_spray) { false } + let(:ignore_public) { nil } + let(:ignore_private) { nil } describe "#each" do specify do @@ -323,6 +327,34 @@ end end + context 'when :ignore_public is true and :username is nil' do + let(:ignore_public) { true } + let(:username) { nil } + specify do + expect { |b| collection.each(&b) }.to_not yield_control + end + end + + context 'when :ignore_private is true and password is nil' do + let(:ignore_private) { true } + let(:password) { nil } + specify do + expect { |b| collection.each(&b) }.to yield_successive_args( + Metasploit::Framework::Credential.new(public: username, private: nil) + ) + end + + context 'when :ignore_public is also true and username is nil' do + let(:ignore_public) { true } + let(:username) { nil } + specify do + expect { |b| collection.each(&b) }.to yield_successive_args( + Metasploit::Framework::Credential.new(public: nil, private: nil) + ) + end + end + end + end describe "#empty?" do @@ -392,6 +424,21 @@ expect(collection.empty?).to eq true end end + + context "and :ignore_public is set" do + let(:ignore_public) { true } + specify do + expect(collection.empty?).to eq true + end + + context "and :ignore_private is also set" do + let(:ignore_private) { true } + specify do + expect(collection.empty?).to eq false + end + end + end + end end end