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