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 PASSWORD_SPRAY being ignored for LDAP (and potetnially other modules) #19079

Merged
merged 10 commits into from
Apr 22, 2024
140 changes: 130 additions & 10 deletions lib/metasploit/framework/credential_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,24 @@ def prepend_cred(cred)
self
end

# Combines all the provided credential sources into a stream of {Credential}
# objects, yielding them one at a time
#
# @yieldparam credential [Metasploit::Framework::Credential]
# @return [void]
def each_filtered
each_unfiltered do |credential|
next unless self.filter.nil? || self.filter.call(credential)
if password_spray
each_unfiltered_password_first do |credential|
next unless self.filter.nil? || self.filter.call(credential)

yield credential
end
else
each_unfiltered_username_first do |credential|
next unless self.filter.nil? || self.filter.call(credential)

yield credential
yield credential
end
end
end

Expand Down Expand Up @@ -164,6 +177,7 @@ def private_type(private)
end

class CredentialCollection < PrivateCredentialCollection
attr_accessor :password_spray

# @!attribute additional_publics
# Additional public values that should be tried
Expand Down Expand Up @@ -219,12 +233,119 @@ def add_public(public_str='')
additional_publics << public_str
end

# Combines all the provided credential sources into a stream of {Credential}
# objects, yielding them one at a time
#
# @yieldparam credential [Metasploit::Framework::Credential]
# @return [void]
def each_unfiltered
# When password spraying is enabled, do first passwords then userames
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# When password spraying is enabled, do first passwords then userames
# When password spraying is enabled, do first passwords then usernames

# i.e.
# username1:password1
# username2:password1
# username3:password1
# ...
# username1:password2
# username2:password2
# username3:password2
# ...
cgranleese-r7 marked this conversation as resolved.
Show resolved Hide resolved
def each_unfiltered_password_first
if user_file.present?
user_fd = File.open(user_file, 'r:binary')
cgranleese-r7 marked this conversation as resolved.
Show resolved Hide resolved
end

prepended_creds.each { |c| yield c }

if anonymous_login
yield Metasploit::Framework::Credential.new(public: '', private: '', realm: realm, private_type: :password)
end

if password.present?
if nil_passwords
yield Metasploit::Framework::Credential.new(public: username, private: nil, realm: realm, private_type: :password)
end
if username.present?
yield Metasploit::Framework::Credential.new(public: username, private: password, realm: realm, private_type: private_type(password))
end
if user_as_pass
yield Metasploit::Framework::Credential.new(public: username, private: username, realm: realm, private_type: :password)
end
if blank_passwords
yield Metasploit::Framework::Credential.new(public: username, private: "", realm: realm, private_type: :password)
end
if user_fd
user_fd.each_line do |user_from_file|
user_from_file.chomp!
yield Metasploit::Framework::Credential.new(public: user_from_file, private: password, realm: realm, private_type: private_type(pass_from_file))
cgranleese-r7 marked this conversation as resolved.
Show resolved Hide resolved
end
user_fd.seek(0)
end
end

if pass_file.present?
File.open(pass_file, 'r:binary') do |pass_fd|
pass_fd.each_line do |pass_from_file|
pass_from_file.chomp!
if user_as_pass
yield Metasploit::Framework::Credential.new(public: pass_from_file, private: pass_from_file, realm: realm, private_type: :password)
end
if user_fd
user_fd.each_line do |user_from_file|
user_from_file.chomp!
yield Metasploit::Framework::Credential.new(public: user_from_file, private: pass_from_file, realm: realm, private_type: private_type(pass_from_file))
end
user_fd.seek(0)
end
additional_privates.each do |add_private|
yield Metasploit::Framework::Credential.new(public: user_from_file, private: add_private, realm: realm, private_type: private_type(add_private))
end
end
end
end

if userpass_file.present?
File.open(userpass_file, 'r:binary') do |userpass_fd|
userpass_fd.each_line do |line|
user, pass = line.split(" ", 2)
if pass.blank?
pass = ''
else
pass.chomp!
end
yield Metasploit::Framework::Credential.new(public: user, private: pass, realm: realm)
end
end
end

additional_publics.each do |add_public|
if password.present?
yield Metasploit::Framework::Credential.new(public: add_public, private: password, realm: realm, private_type: private_type(password) )
end
if user_as_pass
yield Metasploit::Framework::Credential.new(public: add_public, private: user_from_file, realm: realm, private_type: :password)
end
if blank_passwords
yield Metasploit::Framework::Credential.new(public: add_public, private: "", realm: realm, private_type: :password)
end
if user_fd
user_fd.each_line do |user_from_file|
user_from_file.chomp!
yield Metasploit::Framework::Credential.new(public: add_public, private: user_from_file, realm: realm, private_type: private_type(user_from_file))
end
user_fd.seek(0)
end
additional_privates.each do |add_private|
yield Metasploit::Framework::Credential.new(public: add_public, private: add_private, realm: realm, private_type: private_type(add_private))
end
end
ensure
user_fd.close if user_fd && !user_fd.closed?
end

# When password spraying is not enabled, do first usersnames then passwords
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# When password spraying is not enabled, do first usersnames then passwords
# When password spraying is not enabled, do first usernames then passwords

# i.e.
# username1:password1
# username1:password2
# username1:password3
# ...
# username2:password1
# username2:password2
# username2:password3
def each_unfiltered_username_first
cgranleese-r7 marked this conversation as resolved.
Show resolved Hide resolved
if pass_file.present?
pass_fd = File.open(pass_file, 'r:binary')
end
Expand Down Expand Up @@ -325,7 +446,6 @@ def each_unfiltered
yield Metasploit::Framework::Credential.new(public: add_public, private: add_private, realm: realm, private_type: private_type(add_private))
end
end

ensure
pass_fd.close if pass_fd && !pass_fd.closed?
end
Expand Down
1 change: 1 addition & 0 deletions lib/msf/core/auxiliary/auth_brute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def build_credential_collection(opts)
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
user_as_pass: datastore['USER_AS_PASS'],
password_spray: datastore['PASSWORD_SPRAY']
}.merge(opts))

if framework.db.active
Expand Down
3 changes: 2 additions & 1 deletion modules/auxiliary/scanner/ldap/ldap_login.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ def run_host(ip)
password: datastore['PASSWORD'],
realm: datastore['DOMAIN'],
anonymous_login: datastore['ANONYMOUS_LOGIN'],
blank_passwords: false
blank_passwords: false,
password_spray: datastore['PASSWORD_SPRAY']
)

opts = {
Expand Down
36 changes: 34 additions & 2 deletions spec/lib/metasploit/framework/credential_collection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
userpass_file: userpass_file,
prepended_creds: prepended_creds,
additional_privates: additional_privates,
additional_publics: additional_publics
additional_publics: additional_publics,
password_spray: password_spray
)
end

Expand All @@ -30,6 +31,7 @@
let(:prepended_creds) { [] }
let(:additional_privates) { [] }
let(:additional_publics) { [] }
let(:password_spray) { false }

describe "#each" do
specify do
Expand Down Expand Up @@ -72,7 +74,6 @@
end
end


context "when given a userspass_file" do
let(:username) { nil }
let(:password) { nil }
Expand Down Expand Up @@ -120,6 +121,37 @@
end
end

context "when given a pass_file and user_file and password spray" do
let(:password) { nil }
let(:username) { nil }
let(:password_spray) { true }
let(:user_file) do
filename = "user_file"
stub_file = StringIO.new("user1\nuser2\nuser3\n")
allow(File).to receive(:open).with(filename,/^r/).and_yield stub_file

filename
end
let(:pass_file) do
filename = "pass_file"
stub_file = StringIO.new("password1\npassword2\n")
allow(File).to receive(:open).with(filename,/^r/).and_return stub_file
cgranleese-r7 marked this conversation as resolved.
Show resolved Hide resolved

filename
end

specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: "user1", private: "password1"),
Metasploit::Framework::Credential.new(public: "user2", private: "password1"),
Metasploit::Framework::Credential.new(public: "user3", private: "password1"),
Metasploit::Framework::Credential.new(public: "user1", private: "password2"),
Metasploit::Framework::Credential.new(public: "user2", private: "password2"),
Metasploit::Framework::Credential.new(public: "user3", private: "password2"),
)
end
end

context 'when given a username, user_file and pass_file' do
let(:password) { nil }
let(:username) { 'my_username' }
Expand Down
Loading