Skip to content

Commit

Permalink
Merge pull request #19843 from cdelafuente-r7/fix/mod/ldap_smb_login
Browse files Browse the repository at this point in the history
Fix ldap_login and smb_login
  • Loading branch information
jheysel-r7 authored Jan 29, 2025
2 parents 157763b + 1885b65 commit aa78924
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 11 deletions.
29 changes: 26 additions & 3 deletions lib/metasploit/framework/credential_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions modules/auxiliary/scanner/ldap/ldap_login.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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(
Expand Down
18 changes: 15 additions & 3 deletions modules/auxiliary/scanner/smb/smb_login.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
49 changes: 48 additions & 1 deletion spec/lib/metasploit/framework/credential_collection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit aa78924

Please sign in to comment.