Files
metasploit-gs/lib/msf/core/exploit/remote/ldap.rb
T

380 lines
17 KiB
Ruby

# -*- coding: binary -*-
#
# This mixin is a wrapper around Net::LDAP
#
require 'rex/proto/ldap'
module Msf
module Exploit::Remote::LDAP
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
# 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(
[
*kerberos_storage_options(protocol: 'LDAP'),
*kerberos_auth_options(protocol: 'LDAP', auth_methods: Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS),
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
connect_opts = {
host: rhost,
port: rport,
connect_timeout: datastore['LDAP::ConnectTimeout']
}
if datastore['SSL']
connect_opts[:encryption] = {
method: :simple_tls,
tls_options: {
verify_mode: OpenSSL::SSL::VERIFY_NONE
}
}
end
case datastore['LDAP::Auth']
when Msf::Exploit::Remote::AuthOption::KERBEROS
fail_with(Msf::Exploit::Failure::BadConfig, 'The Ldap::Rhostname option is required when using Kerberos authentication.') if datastore['Ldap::Rhostname'].blank?
fail_with(Msf::Exploit::Failure::BadConfig, 'The DOMAIN option is required when using Kerberos authentication.') if datastore['DOMAIN'].blank?
fail_with(Msf::Exploit::Failure::BadConfig, 'The DomainControllerRhost is required when using Kerberos authentication.') if datastore['DomainControllerRhost'].blank?
offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['Ldap::KrbOfferedEncryptionTypes'])
fail_with(Msf::Exploit::Failure::BadConfig, '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: datastore['DomainControllerRhost'],
hostname: datastore['Ldap::Rhostname'],
realm: datastore['DOMAIN'],
username: datastore['USERNAME'],
password: datastore['PASSWORD'],
framework: framework,
framework_module: self,
cache_file: datastore['Ldap::Krb5Ccname'].blank? ? nil : datastore['Ldap::Krb5Ccname'],
ticket_storage: kerberos_ticket_storage,
offered_etypes: offered_etypes
)
kerberos_result = kerberos_authenticator.authenticate
connect_opts[:auth] = {
method: :sasl,
mechanism: 'GSS-SPNEGO',
initial_credential: kerberos_result[:security_blob],
challenge_response: true
}
when Msf::Exploit::Remote::AuthOption::NTLM
ntlm_client = RubySMB::NTLM::Client.new(
datastore['USERNAME'],
datastore['PASSWORD'],
workstation: 'WORKSTATION',
domain: datastore['DOMAIN'].blank? ? '.' : datastore['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
username = datastore['USERNAME'].dup
username << "@#{datastore['DOMAIN']}" unless datastore['DOMAIN'].blank?
connect_opts[:auth] = {
method: :simple,
username: username,
password: datastore['PASSWORD']
}
when Msf::Exploit::Remote::AuthOption::AUTO
unless datastore['USERNAME'].blank? # plaintext if specified
username = datastore['USERNAME'].dup
username << "@#{datastore['DOMAIN']}" unless datastore['DOMAIN'].blank?
connect_opts[:auth] = {
method: :simple,
username: username,
password: datastore['PASSWORD']
}
end
end
connect_opts
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 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_connect(opts = {}, &block)
Net::LDAP.open(get_connect_opts.merge(opts), &block)
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(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 =~ /^(DC=[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.as_json['result']['ldap_result']
# Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes
case bind_result['resultCode']
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['errorMessage'].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['errorMessage'].strip}")
when 14
fail_with(Msf::Module::Failure::NoTarget, "Server requires additional information to complete the bind. Error was: #{bind_result['errorMessage'].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['errorMessage'].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 'resultCode' with an integer representing the result code,
# 'errorMessage' containing an optional error message, and
# 'matchedDN' containing the matched DN.
# @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)
if query_result.class != Hash
raise ArgumentError.new('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['resultCode']
when 0
vprint_status("Successfully queried #{filter}.")
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 on #{filter}. It is likely the client requires authorization! The error was: #{query_result['errorMessage'].strip}")
when 2
fail_with(Msf::Module::Failure::BadConfig, "The LDAP protocol being used by Metasploit isn't supported. The error was #{query_result['errorMessage'].strip}")
when 3
fail_with(Msf::Module::Failure::TimeoutExpired, "The LDAP server returned a timeout response to the query #{filter}.")
when 4
fail_with(Msf::Module::Failure::UnexpectedReply, "The LDAP query #{filter} 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 that #{filter} hit some administrative limit within the server whilst the request was being processed.")
when 16
fail_with(Msf::Module::Failure::NotFound, "The LDAP operation failed for #{filter} because the referenced attribute does not exist.")
when 18
fail_with(Msf::Module::Failure::BadConfig, "The LDAP search with #{filter} failed because some matching is not supported for the target attribute type!")
when 32
fail_with(Msf::Module::Failure::UnexpectedReply, "The LDAP search with #{filter} failed cause 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 deference an alias that didn't resolve properly with #{filter}.")
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 using #{filter}.")
when 65
fail_with(Msf::Module::Failure::Unknown, "The LDAP operation failed due to an object class violation when using #{filter}.")
else
if query_result['errorMessage'].blank?
fail_with(Msf::Module::Failure::Unknown, "Query #{filter} failed but no error message was returned!")
else
fail_with(Msf::Module::Failure::Unknown, "Query #{filter} failed with error: #{query_result['errorMessage'].strip}")
end
end
end
end
end