Module: Msf::Exploit::Remote::LDAP

Includes:
Kerberos::ServiceAuthenticator::Options, Kerberos::Ticket::Storage
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

Methods included from Kerberos::ServiceAuthenticator::Options

#kerberos_auth_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.

Parameters:

  • ldap (Net::LDAP)

    The Net::LDAP connection handle for the current LDAP connection.

Returns:

  • (String)

    A string containing the base DN of the target LDAP server.



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_optsHash

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.

Returns:

  • (Hash)

    The options to use when connecting to the target LDAP server.



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')
      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

#get_naming_contexts(ldap) ⇒ Net::BER::BerIdentifiedArray

Get the naming contexts for the target LDAP server.

Parameters:

  • ldap (Net::LDAP)

    The Net::LDAP connection handle for the current LDAP connection.

Returns:

  • (Net::BER::BerIdentifiedArray)

    Array of 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.

Parameters:

  • info (Hash) (defaults to: {})

    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.



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

  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

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

Parameters:

  • opts (Hash) (defaults to: {})

    Options for the LDAP connection.

  • block (Proc)

    A proc containing the functionality to execute after the LDAP connection has succeeded. The connection is closed once this proc finishes executing.

Returns:

  • (Object)

    The result of whatever the block that was passed in via the "block" parameter yielded.

See Also:

  • Net::LDAP.open


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.

Parameters:

  • opts (Hash) (defaults to: {})

    A hash containing the connection options for the LDAP connection to the target server.

Yield Parameters:

  • ldap (Net::LDAP)

    The LDAP connection handle to use for connecting to the target LDAP server.



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

#peerString

Return the peer as a host:port formatted string.

Returns:

  • (String)

    A string containing the peer details in RHOST:RPORT format.



64
65
66
# File 'lib/msf/core/exploit/remote/ldap.rb', line 64

def peer
  "#{rhost}:#{rport}"
end

#rhostString

Alias to return the RHOST datastore option.

Returns:

  • (String)

    The current value of RHOST in the datastore.



50
51
52
# File 'lib/msf/core/exploit/remote/ldap.rb', line 50

def rhost
  datastore['RHOST']
end

#rportString

Alias to return the RPORT datastore option.

Returns:

  • (String)

    The current value of RPORT in the datastore.



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.

Parameters:

  • ldap (Net::LDAP)

    The Net::LDAP connection handle for the current LDAP connection.

Returns:

  • (Nil)

    This function does not return any data.

Raises:

  • (RuntimeError)

    A RuntimeError will be raised if the LDAP bind request failed.



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.

Parameters:

  • 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.

  • filter (Net::LDAP::Filter) (defaults to: nil)

    A Net::LDAP::Filter to use to filter the results of the query.

Returns:

  • (Nil)

    This function does not return any data.

Raises:

  • (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.



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