Skip to content

Commit

Permalink
Land #18197, Ldap login scanner module
Browse files Browse the repository at this point in the history
Adds a new login scanner module for LDAP
  • Loading branch information
jheysel-r7 committed Oct 2, 2023
2 parents c728671 + 76a25c6 commit 5087e0f
Show file tree
Hide file tree
Showing 9 changed files with 746 additions and 130 deletions.
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
Loading

0 comments on commit 5087e0f

Please sign in to comment.