Module: Msf::Exploit::Remote::LDAP
- Defined in:
- lib/msf/core/exploit/remote/ldap/server.rb,
lib/msf/core/exploit/remote/ldap.rb
Overview
This module exposes methods for querying a remote LDAP service
Defined Under Namespace
Modules: Server
Instance Method Summary collapse
-
#discover_base_dn(ldap) ⇒ String
Discover the base DN of the target LDAP server via the LDAP server’s naming contexts.
-
#get_connect_opts ⇒ Hash
Set the various connection options to use when connecting to the target LDAP server based on the current datastore options.
-
#get_naming_contexts(ldap) ⇒ Net::BER::BerIdentifiedArray
Get the naming contexts for the target LDAP server.
-
#initialize(info = {}) ⇒ Object
Initialize the LDAP client and set up the LDAP specific datastore options to allow the client to perform authentication and timeout operations.
-
#ldap_connect(opts = {}, &block) ⇒ Object
Connect to the target LDAP server using the options provided, and pass the resulting connection object to the proc provided.
-
#ldap_new(opts = {}) {|ldap| ... } ⇒ Object
Create a new LDAP connection using Net::LDAP.new and yield the resulting connection object to the caller of this method.
-
#peer ⇒ String
Return the peer as a host:port formatted string.
-
#rhost ⇒ String
Alias to return the RHOST datastore option.
-
#rport ⇒ String
Alias to return the RPORT datastore option.
-
#validate_bind_success!(ldap) ⇒ Nil
Check whether it was possible to successfully bind to the target LDAP server.
-
#validate_query_result!(query_result, filter = nil) ⇒ Nil
Validate the query result and check whether the query succeeded.
Methods included from Kerberos::ServiceAuthenticator::Options
Methods included from Kerberos::Ticket::Storage
#kerberos_storage_options, #kerberos_ticket_storage, store_ccache
Instance Method Details
#discover_base_dn(ldap) ⇒ String
Discover the base DN of the target LDAP server via the LDAP server’s naming contexts.
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 290 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 |
#get_connect_opts ⇒ Hash
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.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 74 def get_connect_opts connect_opts = { host: rhost, port: rport, proxies: datastore['Proxies'], 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::SCHANNEL pfx_path = datastore['LDAP::CertFile'] fail_with(Msf::Exploit::Remote::Failure::BadConfig, 'The LDAP::CertFile option is required when using SCHANNEL authentication.') if pfx_path.blank? fail_with(Msf::Exploit::Remote::Failure::BadConfig, 'The SSL option must be enabled when using SCHANNEL authentication.') if datastore['SSL'] != true unless ::File.file?(pfx_path) and ::File.readable?(pfx_path) fail_with(Msf::Exploit::Remote::Failure::BadConfig, '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 => e fail_with(Msf::Exploit::Remote::Failure::BadConfig, "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 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'], proxies: datastore['Proxies'], 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') = ntlm_client.init_context(challenge) .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 |
#get_naming_contexts(ldap) ⇒ Net::BER::BerIdentifiedArray
Get the naming contexts for the target LDAP server.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 265 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 |
#initialize(info = {}) ⇒ Object
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.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 24 def initialize(info = {}) super ([ 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']) ]) ( [ Opt::Proxies, *(protocol: 'LDAP'), *(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 |
#ldap_connect(opts = {}, &block) ⇒ Object
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.
215 216 217 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 215 def ldap_connect(opts = {}, &block) Net::LDAP.open(get_connect_opts.merge(opts), &block) end |
#ldap_new(opts = {}) {|ldap| ... } ⇒ Object
Create a new LDAP connection using Net::LDAP.new and yield the resulting connection object to the caller of this method.
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 226 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 |
#peer ⇒ String
Return the peer as a host:port formatted string.
64 65 66 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 64 def peer "#{rhost}:#{rport}" end |
#rhost ⇒ String
Alias to return the RHOST datastore option.
50 51 52 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 50 def rhost datastore['RHOST'] end |
#rport ⇒ String
Alias to return the RPORT datastore option.
57 58 59 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 57 def rport datastore['RPORT'] end |
#validate_bind_success!(ldap) ⇒ Nil
Check whether it was possible to successfully bind to the target LDAP server. Raise a RuntimeException with an appropriate error message if not.
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 322 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_query_result!(query_result, filter = nil) ⇒ Nil
Validate the query result and check whether the query succeeded. Fail with an appropriate error code if the query failed.
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/msf/core/exploit/remote/ldap.rb', line 362 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 |