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

Fix ldap_login and smb_login #19843

Merged
merged 1 commit into from
Jan 29, 2025
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
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
Loading