326 lines
14 KiB
Ruby
326 lines
14 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
#
|
|
# This mixin is a wrapper around Net::LDAP
|
|
#
|
|
|
|
require 'rex/proto/ldap'
|
|
require 'metasploit/framework/ldap/client'
|
|
|
|
module Msf
|
|
module Exploit::Remote::LDAP
|
|
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
|
|
include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
|
|
include Metasploit::Framework::LDAP::Client
|
|
|
|
# Initialize the LDAP client and set up the LDAP specific datastore
|
|
# options to allow the client to perform authentication and timeout
|
|
# operations. Acts as a wrapper around the caller's
|
|
# implementation of the `initialize` method, which will usually be
|
|
# the module's class's implementation, such as lib/msf/core/auxiliary.rb.
|
|
#
|
|
# @param info [Hash] A hash containing information about the module
|
|
# using this library which includes its name, description, author, references,
|
|
# disclosure date, license, actions, default action, default options,
|
|
# and notes.
|
|
def initialize(info = {})
|
|
super
|
|
|
|
register_options([
|
|
Opt::RHOST,
|
|
Opt::RPORT(389),
|
|
OptBool.new('SSL', [false, 'Enable SSL on the LDAP connection', false]),
|
|
Msf::OptString.new('DOMAIN', [false, 'The domain to authenticate to']),
|
|
Msf::OptString.new('USERNAME', [false, 'The username to authenticate with'], aliases: ['BIND_DN']),
|
|
Msf::OptString.new('PASSWORD', [false, 'The password to authenticate with'], aliases: ['BIND_PW'])
|
|
])
|
|
|
|
register_advanced_options(
|
|
[
|
|
Opt::Proxies,
|
|
*kerberos_storage_options(protocol: 'LDAP'),
|
|
*kerberos_auth_options(protocol: 'LDAP', auth_methods: Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS),
|
|
Msf::OptPath.new('LDAP::CertFile', [false, 'The path to the PKCS12 (.pfx) certificate file to authenticate with'], conditions: ['LDAP::Auth', '==', Msf::Exploit::Remote::AuthOption::SCHANNEL]),
|
|
OptFloat.new('LDAP::ConnectTimeout', [true, 'Timeout for LDAP connect', 10.0])
|
|
]
|
|
)
|
|
end
|
|
|
|
# Alias to return the RHOST datastore option.
|
|
#
|
|
# @return [String] The current value of RHOST in the datastore.
|
|
def rhost
|
|
datastore['RHOST']
|
|
end
|
|
|
|
# Alias to return the RPORT datastore option.
|
|
#
|
|
# @return [String] The current value of RPORT in the datastore.
|
|
def rport
|
|
datastore['RPORT']
|
|
end
|
|
|
|
# Return the peer as a host:port formatted string.
|
|
#
|
|
# @return [String] A string containing the peer details in RHOST:RPORT format.
|
|
def peer
|
|
"#{rhost}:#{rport}"
|
|
end
|
|
|
|
# Set the various connection options to use when connecting to the
|
|
# target LDAP server based on the current datastore options. Returns
|
|
# the resulting connection configuration as a hash.
|
|
#
|
|
# @return [Hash] The options to use when connecting to the target
|
|
# LDAP server.
|
|
def get_connect_opts
|
|
opts = {
|
|
username: datastore['USERNAME'],
|
|
password: datastore['PASSWORD'],
|
|
domain: datastore['DOMAIN'],
|
|
domain_controller_rhost: datastore['DomainControllerRhost'],
|
|
ldap_auth: datastore['LDAP::Auth'],
|
|
ldap_cert_file: datastore['LDAP::CertFile'],
|
|
ldap_rhostname: datastore['Ldap::Rhostname'],
|
|
ldap_krb_offered_enc_types: datastore['Ldap::KrbOfferedEncryptionTypes'],
|
|
ldap_krb5_cname: datastore['Ldap::Krb5Ccname'],
|
|
proxies: datastore['Proxies'],
|
|
framework_module: self
|
|
}
|
|
|
|
ldap_connect_opts(rhost, rport, datastore['LDAP::ConnectTimeout'], ssl: datastore['SSL'], opts: opts)
|
|
end
|
|
|
|
# @see #ldap_open
|
|
# @return [Object] The result of whatever the block that was
|
|
# passed in via the "block" parameter yielded.
|
|
def ldap_connect(opts = {}, &block)
|
|
ldap_open(get_connect_opts.merge(opts), &block)
|
|
end
|
|
|
|
# Connect to the target LDAP server using the options provided,
|
|
# and pass the resulting connection object to the proc provided.
|
|
# Terminate the connection once the proc finishes executing.
|
|
#
|
|
# @param connect_opts [Hash] Options for the LDAP connection.
|
|
# @param block [Proc] A proc containing the functionality to execute
|
|
# after the LDAP connection has succeeded. The connection is closed
|
|
# once this proc finishes executing.
|
|
# @see Net::LDAP.open
|
|
# @return [Object] The result of whatever the block that was
|
|
# passed in via the "block" parameter yielded.
|
|
def ldap_open(connect_opts, &block)
|
|
opts = resolve_connect_opts(connect_opts)
|
|
Net::LDAP.open(opts, &block)
|
|
end
|
|
|
|
|
|
def resolve_connect_opts(connect_opts)
|
|
return connect_opts unless connect_opts.dig(:auth, :initial_credential).is_a?(Proc)
|
|
|
|
opts = connect_opts.dup
|
|
# For scenarios such as Kerberos, we might need to make additional calls out to a separate services to acquire an initial credential
|
|
opts[:auth].merge!(
|
|
initial_credential: opts[:auth][:initial_credential].call
|
|
)
|
|
opts
|
|
end
|
|
|
|
# Create a new LDAP connection using Net::LDAP.new and yield the
|
|
# resulting connection object to the caller of this method.
|
|
#
|
|
# @param opts [Hash] A hash containing the connection options for the
|
|
# LDAP connection to the target server.
|
|
# @yieldparam ldap [Net::LDAP] The LDAP connection handle to use for connecting to
|
|
# the target LDAP server.
|
|
def ldap_new(opts = {})
|
|
|
|
ldap = Net::LDAP.new(resolve_connect_opts(get_connect_opts.merge(opts)))
|
|
|
|
# NASTY, but required
|
|
# monkey patch ldap object in order to ignore bind errors
|
|
# Some servers (e.g. OpenLDAP) return result even after a bind
|
|
# has failed, e.g. with LDAP_INAPPROPRIATE_AUTH - anonymous bind disallowed.
|
|
# See: https://www.openldap.org/doc/admin23/security.html#Authentication%20Methods
|
|
# "Note that disabling the anonymous bind mechanism does not prevent anonymous
|
|
# access to the directory."
|
|
# Bug created for Net:LDAP at https://github.com/ruby-ldap/ruby-net-ldap/issues/375
|
|
#
|
|
# @yieldparam conn [Net::LDAP] The LDAP connection handle to use for connecting to
|
|
# the target LDAP server.
|
|
# @param args [Hash] A hash containing options for the ldap connection
|
|
def ldap.use_connection(args)
|
|
if @open_connection
|
|
yield @open_connection
|
|
else
|
|
begin
|
|
conn = new_connection
|
|
conn.bind(args[:auth] || @auth)
|
|
# Commented out vs. original
|
|
# result = conn.bind(args[:auth] || @auth)
|
|
# return result unless result.result_code == Net::LDAP::ResultCodeSuccess
|
|
yield conn
|
|
ensure
|
|
conn.close if conn
|
|
end
|
|
end
|
|
end
|
|
yield ldap
|
|
end
|
|
|
|
# Get the naming contexts for the target LDAP server.
|
|
#
|
|
# @param ldap [Net::LDAP] The Net::LDAP connection handle for the
|
|
# current LDAP connection.
|
|
# @return [Net::BER::BerIdentifiedArray] Array of naming contexts for the target LDAP server.
|
|
def get_naming_contexts(ldap)
|
|
vprint_status("#{peer} Getting root DSE")
|
|
|
|
unless (root_dse = ldap.search_root_dse)
|
|
print_error("#{peer} Could not retrieve root DSE")
|
|
return
|
|
end
|
|
|
|
naming_contexts = root_dse[:namingcontexts]
|
|
|
|
# NOTE: Net::LDAP converts attribute names to lowercase
|
|
if naming_contexts.empty?
|
|
print_error("#{peer} Empty namingContexts attribute")
|
|
return
|
|
end
|
|
|
|
naming_contexts
|
|
end
|
|
|
|
# Discover the base DN of the target LDAP server via the LDAP
|
|
# server's naming contexts.
|
|
#
|
|
# @param ldap [Net::LDAP] The Net::LDAP connection handle for the
|
|
# current LDAP connection.
|
|
# @return [String] A string containing the base DN of the target LDAP server.
|
|
def discover_base_dn(ldap)
|
|
# @type [Net::BER::BerIdentifiedArray]
|
|
naming_contexts = get_naming_contexts(ldap)
|
|
|
|
unless naming_contexts
|
|
print_error("#{peer} Base DN cannot be determined")
|
|
return
|
|
end
|
|
|
|
# NOTE: Find the first entry that starts with `DC=` as this will likely be the base DN.
|
|
naming_contexts.select! { |context| context =~ /^([Dd][Cc]=[A-Za-z0-9-]+,?)+$/ }
|
|
naming_contexts.reject! { |context| context =~ /(Configuration)|(Schema)|(ForestDnsZones)/ }
|
|
if naming_contexts.blank?
|
|
print_error("#{peer} A base DN matching the expected format could not be found!")
|
|
return
|
|
end
|
|
base_dn = naming_contexts[0]
|
|
|
|
print_good("#{peer} Discovered base DN: #{base_dn}")
|
|
base_dn
|
|
end
|
|
|
|
# Check whether it was possible to successfully bind to the target LDAP
|
|
# server. Raise a RuntimeException with an appropriate error message
|
|
# if not.
|
|
#
|
|
# @param ldap [Net::LDAP] The Net::LDAP connection handle for the
|
|
# current LDAP connection.
|
|
#
|
|
# @raise [RuntimeError] A RuntimeError will be raised if the LDAP
|
|
# bind request failed.
|
|
# @return [Nil] This function does not return any data.
|
|
def validate_bind_success!(ldap)
|
|
bind_result = ldap.get_operation_result.table
|
|
|
|
# Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes
|
|
case bind_result[:code]
|
|
when 0
|
|
vprint_good('Successfully bound to the LDAP server!')
|
|
when 1
|
|
fail_with(Msf::Module::Failure::NoAccess, "An operational error occurred, perhaps due to lack of authorization. The error was: #{bind_result[:error_message].strip}")
|
|
when 7
|
|
fail_with(Msf::Module::Failure::NoTarget, 'Target does not support the simple authentication mechanism!')
|
|
when 8
|
|
fail_with(Msf::Module::Failure::NoTarget, "Server requires a stronger form of authentication than we can provide! The error was: #{bind_result[:error_message].strip}")
|
|
when 14
|
|
fail_with(Msf::Module::Failure::NoTarget, "Server requires additional information to complete the bind. Error was: #{bind_result[:error_message].strip}")
|
|
when 48
|
|
fail_with(Msf::Module::Failure::NoAccess, "Target doesn't support the requested authentication type we sent. Try binding to the same user without a password, or providing credentials if you were doing anonymous authentication.")
|
|
when 49
|
|
fail_with(Msf::Module::Failure::NoAccess, 'Invalid credentials provided!')
|
|
else
|
|
fail_with(Msf::Module::Failure::Unknown, "Unknown error occurred whilst binding: #{bind_result[:error_message].strip}")
|
|
end
|
|
end
|
|
|
|
# Validate the query result and check whether the query succeeded.
|
|
# Fail with an appropriate error code if the query failed.
|
|
#
|
|
# @param query_result [Hash] A hash containing the results of the query
|
|
# as a 'extended_response' representing the extended response,
|
|
# a 'code' with an integer representing the result code,
|
|
# a 'error_message' containing an optional error message as a Net::BER::BerIdentifiedString,
|
|
# a 'matched_dn' containing the matched DN,
|
|
# and a 'message' containing the query result message.
|
|
# @param filter [Net::LDAP::Filter] A Net::LDAP::Filter to use to
|
|
# filter the results of the query.
|
|
#
|
|
# @raise [RuntimeError, ArgumentError] A RuntimeError will be raised if the LDAP
|
|
# request failed. Alternatively, if the query_result parameter isn't a hash, then an
|
|
# ArgumentError will be raised.
|
|
# @return [Nil] This function does not return any data.
|
|
def validate_query_result!(query_result, filter=nil)
|
|
if query_result.class != Hash
|
|
raise ArgumentError, 'Parameter to "validate_query_result!" function was not a Hash!'
|
|
end
|
|
|
|
# Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes
|
|
case query_result[:code]
|
|
when 0
|
|
vprint_status("Successfully queried #{filter}.") if filter.present?
|
|
when 1
|
|
# This is unknown as whilst we could fail on lack of authorization, this is not guaranteed with this error code.
|
|
# The user will need to inspect the error message to determine the root cause of the issue.
|
|
fail_with(Msf::Module::Failure::Unknown, "An LDAP operational error occurred. It is likely the client requires authorization! The error was: #{query_result[:error_message].strip}")
|
|
when 2
|
|
fail_with(Msf::Module::Failure::BadConfig, "The LDAP protocol being used by Metasploit isn't supported. The error was #{query_result[:error_message].strip}")
|
|
when 3
|
|
fail_with(Msf::Module::Failure::TimeoutExpired, 'The LDAP server returned a timeout response to the query.')
|
|
when 4
|
|
fail_with(Msf::Module::Failure::UnexpectedReply, 'The LDAP query was determined to result in too many entries for the LDAP server to return.')
|
|
when 11
|
|
fail_with(Msf::Module::Failure::UnexpectedReply, 'The LDAP server indicated some administrative limit within the server whilst the request was being processed.')
|
|
when 16
|
|
fail_with(Msf::Module::Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist.')
|
|
when 18
|
|
fail_with(Msf::Module::Failure::BadConfig, 'The LDAP search failed because some matching is not supported for the target attribute type!')
|
|
when 32
|
|
fail_with(Msf::Module::Failure::UnexpectedReply, 'The LDAP search failed because the operation targeted an entity within the base DN that does not exist.')
|
|
when 33
|
|
fail_with(Msf::Module::Failure::BadConfig, "An attempt was made to dereference an alias that didn't resolve properly.")
|
|
when 34
|
|
fail_with(Msf::Module::Failure::BadConfig, 'The request included an invalid base DN entry.')
|
|
when 50
|
|
fail_with(Msf::Module::Failure::NoAccess, 'The LDAP operation failed due to insufficient access rights.')
|
|
when 51
|
|
fail_with(Msf::Module::Failure::UnexpectedReply, 'The LDAP operation failed because the server is too busy to perform the request.')
|
|
when 52
|
|
fail_with(Msf::Module::Failure::UnexpectedReply, 'The LDAP operation failed because the server is not currently available to process the request.')
|
|
when 53
|
|
fail_with(Msf::Module::Failure::UnexpectedReply, 'The LDAP operation failed because the server is unwilling to perform the request.')
|
|
when 64
|
|
fail_with(Msf::Module::Failure::Unknown, 'The LDAP operation failed due to a naming violation.')
|
|
when 65
|
|
fail_with(Msf::Module::Failure::Unknown, 'The LDAP operation failed due to an object class violation.')
|
|
else
|
|
if query_result[:error_message].blank?
|
|
fail_with(Msf::Module::Failure::Unknown, 'The LDAP operation failed but no error message was returned!')
|
|
else
|
|
fail_with(Msf::Module::Failure::Unknown, "The LDAP operation failed with error: #{query_result[:error_message].strip}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|