Skip to content

Commit aa78924

Browse files
authored
Merge pull request rapid7#19843 from cdelafuente-r7/fix/mod/ldap_smb_login
Fix ldap_login and smb_login
2 parents 157763b + 1885b65 commit aa78924

File tree

4 files changed

+106
-11
lines changed

4 files changed

+106
-11
lines changed

lib/metasploit/framework/credential_collection.rb

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,23 @@ class CredentialCollection < PrivateCredentialCollection
212212
# @return [Boolean]
213213
attr_accessor :anonymous_login
214214

215+
# @!attribute ignore_private
216+
# Whether to ignore private (password). This is usually set when Kerberos
217+
# or Schannel authentication is requested and the credentials are
218+
# retrieved from cache or from a file. This attribute should be true in
219+
# these scenarios, otherwise validation will fail since the password is not
220+
# provided.
221+
# @return [Boolean]
222+
attr_accessor :ignore_private
223+
224+
# @!attribute ignore_public
225+
# Whether to ignore public (username). This is usually set when Schannel
226+
# authentication is requested and the credentials are retrieved from a
227+
# file (certificate). This attribute should be true in this case,
228+
# otherwise validation will fail since the password is not provided.
229+
# @return [Boolean]
230+
attr_accessor :ignore_public
231+
215232
# @option opts [Boolean] :blank_passwords See {#blank_passwords}
216233
# @option opts [String] :pass_file See {#pass_file}
217234
# @option opts [String] :password See {#password}
@@ -240,7 +257,13 @@ def add_public(public_str='')
240257
# @yieldparam credential [Metasploit::Framework::Credential]
241258
# @return [void]
242259
def each_filtered
243-
if password_spray
260+
if ignore_private
261+
if ignore_public
262+
yield Metasploit::Framework::Credential.new(public: nil, private: nil, realm: realm)
263+
else
264+
yield Metasploit::Framework::Credential.new(public: username, private: nil, realm: realm)
265+
end
266+
elsif password_spray
244267
each_unfiltered_password_first do |credential|
245268
next unless self.filter.nil? || self.filter.call(credential)
246269

@@ -510,14 +533,14 @@ def empty?
510533
#
511534
# @return [Boolean]
512535
def has_users?
513-
username.present? || user_file.present? || userpass_file.present? || !additional_publics.empty?
536+
username.present? || user_file.present? || userpass_file.present? || !additional_publics.empty? || !!ignore_public
514537
end
515538

516539
# Returns true when there are any private values set
517540
#
518541
# @return [Boolean]
519542
def has_privates?
520-
super || userpass_file.present? || user_as_pass
543+
super || userpass_file.present? || user_as_pass || !!ignore_private
521544
end
522545

523546
end

modules/auxiliary/scanner/ldap/ldap_login.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,19 @@ def validate_connect_options!
8989
end
9090

9191
def run_host(ip)
92+
ignore_public = datastore['LDAP::Auth'] == Msf::Exploit::Remote::AuthOption::SCHANNEL
93+
ignore_private =
94+
datastore['LDAP::Auth'] == Msf::Exploit::Remote::AuthOption::SCHANNEL ||
95+
(Msf::Exploit::Remote::AuthOption::KERBEROS && !datastore['ANONYMOUS_LOGIN'] && !datastore['PASSWORD'])
96+
9297
cred_collection = build_credential_collection(
9398
username: datastore['USERNAME'],
9499
password: datastore['PASSWORD'],
95100
realm: datastore['DOMAIN'],
96101
anonymous_login: datastore['ANONYMOUS_LOGIN'],
97-
blank_passwords: false
102+
blank_passwords: false,
103+
ignore_public: ignore_public,
104+
ignore_private: ignore_private
98105
)
99106

100107
opts = {
@@ -107,14 +114,20 @@ def run_host(ip)
107114
ldap_cert_file: datastore['LDAP::CertFile'],
108115
ldap_rhostname: datastore['Ldap::Rhostname'],
109116
ldap_krb_offered_enc_types: datastore['Ldap::KrbOfferedEncryptionTypes'],
110-
ldap_krb5_cname: datastore['Ldap::Krb5Ccname'],
111-
# Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module
112-
kerberos_ticket_storage: kerberos_ticket_storage({ read: false, write: true })
117+
ldap_krb5_cname: datastore['Ldap::Krb5Ccname']
113118
}
114119

115120
realm_key = nil
116121
if opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::KERBEROS
117122
realm_key = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
123+
if !datastore['ANONYMOUS_LOGIN'] && !datastore['PASSWORD']
124+
# In case no password has been provided, we assume the user wants to use Kerberos tickets stored in cache
125+
# Write mode is still enable in case new TGS tickets are retrieved.
126+
opts[:kerberos_ticket_storage] = kerberos_ticket_storage({ read: true, write: true })
127+
else
128+
# Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module
129+
opts[:kerberos_ticket_storage] = kerberos_ticket_storage({ read: false, write: true })
130+
end
118131
end
119132

120133
scanner = Metasploit::Framework::LoginScanner::LDAP.new(

modules/auxiliary/scanner/smb/smb_login.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ def run_host(ip)
116116
fail_with(Msf::Exploit::Failure::BadConfig, 'The SMBDomain option is required when using Kerberos authentication.') if datastore['SMBDomain'].blank?
117117
fail_with(Msf::Exploit::Failure::BadConfig, 'The DomainControllerRhost is required when using Kerberos authentication.') if datastore['DomainControllerRhost'].blank?
118118

119+
if !datastore['PASSWORD']
120+
# In case no password has been provided, we assume the user wants to use Kerberos tickets stored in cache
121+
# Write mode is still enable in case new TGS tickets are retrieved.
122+
ticket_storage = kerberos_ticket_storage({ read: true, write: true })
123+
else
124+
# Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module
125+
ticket_storage = kerberos_ticket_storage({ read: false, write: true })
126+
end
127+
119128
kerberos_authenticator_factory = lambda do |username, password, realm|
120129
Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::SMB.new(
121130
host: datastore['DomainControllerRhost'],
@@ -127,8 +136,7 @@ def run_host(ip)
127136
framework: framework,
128137
framework_module: self,
129138
cache_file: datastore['Smb::Krb5Ccname'].blank? ? nil : datastore['Smb::Krb5Ccname'],
130-
# Write only cache so we keep all gathered tickets but don't reuse them for auth while running the module
131-
ticket_storage: kerberos_ticket_storage({ read: false, write: true })
139+
ticket_storage: ticket_storage
132140
)
133141
end
134142
end
@@ -170,7 +178,8 @@ def run_host(ip)
170178
cred_collection = build_credential_collection(
171179
realm: domain,
172180
username: datastore['SMBUser'],
173-
password: datastore['SMBPass']
181+
password: datastore['SMBPass'],
182+
ignore_private: datastore['SMB::Auth'] == Msf::Exploit::Remote::AuthOption::KERBEROS && !datastore['PASSWORD']
174183
)
175184
cred_collection = prepend_db_hashes(cred_collection)
176185

@@ -256,6 +265,9 @@ def accepts_bogus_domains?(user, pass)
256265
end
257266

258267
def report_creds(ip, port, result)
268+
# Private can be nil if we authenticated with Kerberos and a cached ticket was used. No need to report this.
269+
return unless result.credential.private
270+
259271
if !datastore['RECORD_GUEST'] && (result.access_level == Metasploit::Framework::LoginScanner::SMB::AccessLevels::GUEST)
260272
return
261273
end

spec/lib/metasploit/framework/credential_collection_spec.rb

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
prepended_creds: prepended_creds,
1717
additional_privates: additional_privates,
1818
additional_publics: additional_publics,
19-
password_spray: password_spray
19+
password_spray: password_spray,
20+
ignore_public: ignore_public,
21+
ignore_private: ignore_private
2022
)
2123
end
2224

@@ -39,6 +41,8 @@
3941
let(:additional_privates) { [] }
4042
let(:additional_publics) { [] }
4143
let(:password_spray) { false }
44+
let(:ignore_public) { nil }
45+
let(:ignore_private) { nil }
4246

4347
describe "#each" do
4448
specify do
@@ -323,6 +327,34 @@
323327
end
324328
end
325329

330+
context 'when :ignore_public is true and :username is nil' do
331+
let(:ignore_public) { true }
332+
let(:username) { nil }
333+
specify do
334+
expect { |b| collection.each(&b) }.to_not yield_control
335+
end
336+
end
337+
338+
context 'when :ignore_private is true and password is nil' do
339+
let(:ignore_private) { true }
340+
let(:password) { nil }
341+
specify do
342+
expect { |b| collection.each(&b) }.to yield_successive_args(
343+
Metasploit::Framework::Credential.new(public: username, private: nil)
344+
)
345+
end
346+
347+
context 'when :ignore_public is also true and username is nil' do
348+
let(:ignore_public) { true }
349+
let(:username) { nil }
350+
specify do
351+
expect { |b| collection.each(&b) }.to yield_successive_args(
352+
Metasploit::Framework::Credential.new(public: nil, private: nil)
353+
)
354+
end
355+
end
356+
end
357+
326358
end
327359

328360
describe "#empty?" do
@@ -392,6 +424,21 @@
392424
expect(collection.empty?).to eq true
393425
end
394426
end
427+
428+
context "and :ignore_public is set" do
429+
let(:ignore_public) { true }
430+
specify do
431+
expect(collection.empty?).to eq true
432+
end
433+
434+
context "and :ignore_private is also set" do
435+
let(:ignore_private) { true }
436+
specify do
437+
expect(collection.empty?).to eq false
438+
end
439+
end
440+
end
441+
395442
end
396443
end
397444
end

0 commit comments

Comments
 (0)