# -*- 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