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

Ldap login scanner module #18197

Merged
merged 10 commits into from
Oct 2, 2023
11 changes: 10 additions & 1 deletion lib/metasploit/framework/credential_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ class CredentialCollection < PrivateCredentialCollection
# @return [String]
attr_accessor :userpass_file

# @!attribute anonymous_login
# Whether to attempt an anonymous login (blank user/pass)
# @return [Boolean]
attr_accessor :anonymous_login

# @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 @@ -226,6 +231,10 @@ def each_unfiltered

prepended_creds.each { |c| yield c }

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

if username.present?
if nil_passwords
yield Metasploit::Framework::Credential.new(public: username, private: nil, realm: realm, private_type: :password)
Expand Down Expand Up @@ -325,7 +334,7 @@ def each_unfiltered
#
# @return [Boolean]
def empty?
prepended_creds.empty? && !has_users? || (has_users? && !has_privates?)
prepended_creds.empty? && !has_users? && !anonymous_login || (has_users? && !has_privates?)
end

# Returns true when there are any user values set
Expand Down
137 changes: 137 additions & 0 deletions lib/metasploit/framework/ldap/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# frozen_string_literal: true

module Metasploit
module Framework
module LDAP

module Client
def ldap_connect_opts(rhost, rport, connect_timeout, ssl: true, opts: {})
connect_opts = {
host: rhost,
port: rport,
connect_timeout: connect_timeout,
proxies: opts[:proxies]
}

if ssl
connect_opts[:encryption] = {
method: :simple_tls,
tls_options: {
verify_mode: OpenSSL::SSL::VERIFY_NONE
}
}
end

case opts[:ldap_auth]
when Msf::Exploit::Remote::AuthOption::SCHANNEL
pfx_path = opts[:ldap_cert_file]
raise Msf::ValidationError, 'The LDAP::CertFile option is required when using SCHANNEL authentication.' if pfx_path.blank?
raise Msf::ValidationError, 'The SSL option must be enabled when using SCHANNEL authentication.' if ssl != true

unless ::File.file?(pfx_path) && ::File.readable?(pfx_path)
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.'
end

begin
pkcs = OpenSSL::PKCS12.new(File.binread(pfx_path), '')
rescue StandardError => e
raise Msf::ValidationError, "Failed to load the PFX file (#{e})"
end

connect_opts[:auth] = {
method: :sasl,
mechanism: 'EXTERNAL',
initial_credential: '',
challenge_response: true
}
connect_opts[:encryption] = {
method: :start_tls,
tls_options: {
verify_mode: OpenSSL::SSL::VERIFY_NONE,
cert: pkcs.certificate,
key: pkcs.key
}
}
when Msf::Exploit::Remote::AuthOption::KERBEROS
raise Msf::ValidationError, 'The Ldap::Rhostname option is required when using Kerberos authentication.' if opts[:ldap_rhostname].blank?
raise Msf::ValidationError, 'The DOMAIN option is required when using Kerberos authentication.' if opts[:domain].blank?
raise Msf::ValidationError, 'The DomainControllerRhost is required when using Kerberos authentication.' if opts[:domain_controller_rhost].blank?

offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(opts[:ldap_krb_offered_enc_types])
raise Msf::ValidationError, 'At least one encryption type is required when using Kerberos authentication.' if offered_etypes.empty?

kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::LDAP.new(
host: opts[:domain_controller_rhost],
hostname: opts[:ldap_rhostname],
realm: opts[:domain],
username: opts[:username],
password: opts[:password],
framework: opts[:framework],
framework_module: opts[:framework_module],
cache_file: opts[:ldap_krb5_cname].blank? ? nil : opts[:ldap_krb5_cname],
ticket_storage: opts[:kerberos_ticket_storage],
offered_etypes: offered_etypes
)

connect_opts[:auth] = {
method: :sasl,
mechanism: 'GSS-SPNEGO',
initial_credential: proc do
kerberos_result = kerberos_authenticator.authenticate
kerberos_result[:security_blob]
end,
challenge_response: true
}
when Msf::Exploit::Remote::AuthOption::NTLM
ntlm_client = RubySMB::NTLM::Client.new(
opts[:username],
opts[:password],
workstation: 'WORKSTATION',
domain: opts[:domain].blank? ? '.' : opts[:domain],
flags:
RubySMB::NTLM::NEGOTIATE_FLAGS[:UNICODE] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:REQUEST_TARGET] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:NTLM] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:ALWAYS_SIGN] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:KEY_EXCHANGE] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:TARGET_INFO] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:VERSION_INFO]
)

negotiate = proc do |challenge|
ntlmssp_offset = challenge.index('NTLMSSP')
type2_blob = challenge.slice(ntlmssp_offset..-1)
challenge = [type2_blob].pack('m')
type3_message = ntlm_client.init_context(challenge)
type3_message.serialize
end

connect_opts[:auth] = {
method: :sasl,
mechanism: 'GSS-SPNEGO',
initial_credential: ntlm_client.init_context.serialize,
challenge_response: negotiate
}
when Msf::Exploit::Remote::AuthOption::PLAINTEXT
connect_opts[:auth] = {
method: :simple,
username: opts[:username],
password: opts[:password]
}
when Msf::Exploit::Remote::AuthOption::AUTO
unless opts[:username].blank? # plaintext if specified
connect_opts[:auth] = {
method: :simple,
username: opts[:username],
password: opts[:password]
}
end
end

connect_opts
end
end
end
end
end
93 changes: 93 additions & 0 deletions lib/metasploit/framework/login_scanner/ldap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

require 'metasploit/framework/login_scanner/base'
require 'metasploit/framework/ldap/client'

module Metasploit
module Framework
module LoginScanner
class LDAP
include Metasploit::Framework::LoginScanner::Base
include Metasploit::Framework::LDAP::Client
include Msf::Exploit::Remote::LDAP

attr_accessor :opts
attr_accessor :realm_key

def attempt_login(credential)
result_opts = {
credential: credential,
status: Metasploit::Model::Login::Status::INCORRECT,
proof: nil,
host: host,
port: port,
protocol: 'ldap'
}

result_opts.merge!(do_login(credential))
Result.new(result_opts)
end

def do_login(credential)
opts = {
username: credential.public,
password: credential.private,
framework_module: framework_module
}.merge(@opts)

connect_opts = ldap_connect_opts(host, port, connection_timeout, ssl: opts[:ssl], opts: opts)
ldap_open(connect_opts) do |ldap|
return status_code(ldap.get_operation_result.table)
rescue StandardError => e
{ status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e }
end
end

def status_code(operation_result)
case operation_result[:code]
when 0
{ status: Metasploit::Model::Login::Status::SUCCESSFUL }
else
{ status: Metasploit::Model::Login::Status::INCORRECT, proof: "Bind Result: #{operation_result}" }
end
end

def each_credential
cred_details.each do |raw_cred|
# This could be a Credential object, or a Credential Core, or an Attempt object
# so make sure that whatever it is, we end up with a Credential.
credential = raw_cred.to_credential

if opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::KERBEROS && opts[:ldap_krb5_cname]
# If we're using kerberos auth with a ccache then the password is irrelevant
# Remove it from the credential so we don't store it
credential.private = nil
elsif opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::SCHANNEL
# If we're using kerberos auth with schannel then the user/password is irrelevant
# Remove it from the credential so we don't store it
credential.public = nil
credential.private = nil
end

if credential.realm.present? && realm_key.present?
credential.realm_key = realm_key
elsif credential.realm.present? && realm_key.blank?
# This service has no realm key, so the realm will be
# meaningless. Strip it off.
credential.realm = nil
credential.realm_key = nil
end

yield credential

if opts[:append_domain] && credential.realm.nil?
credential.public = "#{credential.public}@#{opts[:domain]}"
yield credential
end

end
end
end
end
end
end
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 @@ -27,6 +27,7 @@ def initialize(info = {})
OptBool.new('DB_ALL_PASS', [false,"Add all passwords in the current database to the list",false]),
OptEnum.new('DB_SKIP_EXISTING', [false,"Skip existing credentials stored in the current database", 'none', %w[ none user user&realm ]]),
OptBool.new('STOP_ON_SUCCESS', [ true, "Stop guessing when a credential works for a host", false]),
OptBool.new('ANONYMOUS_LOGIN', [ true, "Attempt to login with a blank username and password", false])
], Auxiliary::AuthBrute)

register_advanced_options([
Expand Down