-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #18197, Ldap login scanner module
Adds a new login scanner module for LDAP
- Loading branch information
Showing
9 changed files
with
746 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.