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

Includes:
Metasploit::Framework::LDAP::Client, CertificateTrace, Kerberos::ServiceAuthenticator::Options, Kerberos::Ticket::Storage
Included in:
Metasploit::Framework::LoginScanner::LDAP, ActiveDirectory
Defined in:
lib/msf/core/exploit/remote/ldap/server.rb,
lib/msf/core/exploit/remote/ldap.rb,
lib/msf/core/exploit/remote/ldap/error.rb,
lib/msf/core/exploit/remote/ldap/queries.rb,
lib/msf/core/exploit/remote/ldap/entry_cache.rb,
lib/msf/core/exploit/remote/ldap/active_directory.rb

Overview

This module exposes methods for querying a remote LDAP service

Defined Under Namespace

Modules: ActiveDirectory, EntryCache, Queries, Server Classes: Error

Instance Method Summary collapse

Methods included from CertificateTrace

#certificate_trace, #certificate_trace_enabled?

Methods included from Metasploit::Framework::LDAP::Client

#ldap_connect_opts

Methods included from Kerberos::ServiceAuthenticator::Options

#kerberos_auth_options, #kerberos_clock_skew_seconds

Methods included from Kerberos::Ticket::Storage

#kerberos_storage_options, #kerberos_ticket_storage, store_ccache

Instance Method Details

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



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
# File 'lib/msf/core/exploit/remote/ldap.rb', line 89

def get_connect_opts
  pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework, framework_module: self)
  ldap_pkcs12 = datastore['LDAP::CertFile'] ? pkcs12_storage.read_pkcs12_cert_path(datastore['LDAP::CertFile']) : nil

  # Trace the client certificate being submitted for LDAP Schannel authentication.
  # Surfaces cert details at the moment the certificate is loaded for the LDAP bind.
  pfx = ldap_pkcs12.is_a?(Hash) ? ldap_pkcs12[:value] : nil
  certificate_trace(pfx.certificate) if pfx.respond_to?(:certificate)

  opts = {
    username: datastore['LDAPUsername'],
    password: datastore['LDAPPassword'],
    domain: datastore['LDAPDomain'],
    base: datastore['BASE_DN'],
    domain_controller_rhost: datastore['DomainControllerRhost'],
    ldap_auth: datastore['LDAP::Auth'],
    ldap_pkcs12: ldap_pkcs12,
    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,
    kerberos_ticket_storage: kerberos_ticket_storage,
    kerberos_clock_skew: kerberos_clock_skew_seconds
  }
  case datastore['LDAP::Signing']
  when 'required'
    opts[:sign_and_seal] = true
  when 'disabled'
    opts[:sign_and_seal] = false
  end

  begin
    result = ldap_connect_opts(rhost, rport, datastore['LDAP::ConnectTimeout'], ssl: ldap_client_ssl, opts: opts)
  rescue Msf::ValidationError => e
    fail_with(Msf::Module::Failure::BadConfig, e.message)
  end

  # Now that the options have been resolved (including auto possibly resolving to NTLM), check whether this is a valid config
  if result[:auth] && datastore['LDAP::Signing'] == 'required'
    unless %i[ rex_kerberos rex_ntlm ].include?(result[:auth][:method]) || (result[:auth][:method] == :sasl && result[:auth][:mechanism] == 'GSS-SPNEGO')
      fail_with(Msf::Module::Failure::BadConfig, 'The authentication configuration does not support signing. Change either LDAP::Auth or LDAP::Signing.')
    end

    if result[:encryption]
      # Domain Controllers don't seem to support signing and connection over SSL. Gotta pick one or the other.
      fail_with(Msf::Module::Failure::BadConfig, 'SSL not supported with signing. Change either SSL or LDAP::Signing.')
    end
  end

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



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/msf/core/exploit/remote/ldap.rb', line 27

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('LDAPDomain', [false, 'The domain to authenticate to'], fallbacks: ['DOMAIN']),
    Msf::OptString.new('LDAPUsername', [false, 'The username to authenticate with'], fallbacks: %w[USERNAME BIND_DN]),
    Msf::OptString.new('LDAPPassword', [false, 'The password to authenticate with'], fallbacks: %w[PASSWORD 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::OptPkcs12Cert.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]),
      OptEnum.new('LDAP::Signing', [true, 'Use signed and sealed (encrypted) LDAP', 'auto', %w[ disabled auto required ]]),
      # Re-register the CertificateTrace options (provided unconditionally by the
      # CertificateTrace mixin) to gate them on Schannel auth, since LDAP only loads
      # a client certificate on that path. This overrides the mixin's registration.
      OptEnum.new('CertificateTrace', [false, 'Certificate trace verbosity level', 'off', %w[off metadata full]], conditions: ['LDAP::Auth', '==', Msf::Exploit::Remote::AuthOption::SCHANNEL]),
      OptString.new('CertificateTraceColors', [false, 'Certificate trace color (e.g. red/blu, unset to disable)', 'red/blu'], conditions: ['LDAP::Auth', '==', Msf::Exploit::Remote::AuthOption::SCHANNEL])
    ]
  )
end

#ldap_connect(opts = {}, &block) ⇒ Object

Returns The result of whatever the block that was passed in via the “block” parameter yielded.

Returns:

  • (Object)

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

See Also:



145
146
147
# File 'lib/msf/core/exploit/remote/ldap.rb', line 145

def ldap_connect(opts = {}, &block)
  ldap_open(get_connect_opts.merge(opts), &block)
end

#ldap_escape_filter(string) ⇒ Object

Return a string suitable for placement in an LDAP filter e.g. (certificateTemplates=#ldap_escape_string(name))

Parameters:

  • string

    String The string to escape.

Returns:

  • The escaped string.



358
359
360
# File 'lib/msf/core/exploit/remote/ldap.rb', line 358

def ldap_escape_filter(string)
  Net::LDAP::Filter.escape(string)
end

#ldap_new(opts = {}) {|ldap| ... } ⇒ Object

Create a new LDAP connection using Rex::Proto::LDAP::Client.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:



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/msf/core/exploit/remote/ldap.rb', line 189

def ldap_new(opts = {})
  ldap = Rex::Proto::LDAP::Client.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
  # Also used to support multi-threading (used for keep-alive)
  #
  # @yieldparam conn [Rex::Proto::LDAP::Client] 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
      register_interaction
    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 == Rex::Proto::LDAP::Client::ResultCodeSuccess
        yield conn
      ensure
        conn.close if conn
      end
    end
  end
  yield ldap
end

#ldap_open(connect_opts, keep_open: false, &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 unless ‘keep_open` is set to true

Parameters:

  • connect_opts (Hash)

    Options for the LDAP connection.

  • keep_open (Boolean) (defaults to: false)

    Keep the connection open or close once the block is finished

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

  • Rex::Proto::LDAP::Client.open


162
163
164
165
166
167
168
169
# File 'lib/msf/core/exploit/remote/ldap.rb', line 162

def ldap_open(connect_opts, keep_open: false, &block)
  opts = resolve_connect_opts(connect_opts)
  if keep_open
    Rex::Proto::LDAP::Client._open(opts, &block)
  else
    Rex::Proto::LDAP::Client.open(opts, &block)
  end
end

#peerString

Return the peer as a host:port formatted string.

Returns:

  • (String)

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



79
80
81
# File 'lib/msf/core/exploit/remote/ldap.rb', line 79

def peer
  Rex::Socket.to_authority(rhost, rport)
end

#report_ldap_serviceObject



268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/msf/core/exploit/remote/ldap.rb', line 268

def report_ldap_service
  host = session ? session.client.peerhost : rhost
  port = session ? session.client.peerport : rport
  transport_srv = { name: 'tcp', host: host, port: port, proto: 'tcp', parents: nil }
  parents = ssl ? { name: 'ssl', host: host, port: port, proto: 'tcp', parents: [transport_srv] } : [transport_srv]

  report_service(
    host: host,
    port: port,
    proto: 'tcp',
    name: 'ldap',
    parents: parents
  )
end

#resolve_connect_opts(connect_opts) ⇒ Object



171
172
173
174
175
176
177
178
179
180
# File 'lib/msf/core/exploit/remote/ldap.rb', line 171

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

#rhostString

Alias to return the RHOST datastore option.

Returns:

  • (String)

    The current value of RHOST in the datastore.



59
60
61
# File 'lib/msf/core/exploit/remote/ldap.rb', line 59

def rhost
  datastore['RHOST']
end

#rportString

Alias to return the RPORT datastore option.

Returns:

  • (String)

    The current value of RPORT in the datastore.



66
67
68
# File 'lib/msf/core/exploit/remote/ldap.rb', line 66

def rport
  datastore['RPORT']
end

#sslObject Also known as: ldap_client_ssl



70
71
72
# File 'lib/msf/core/exploit/remote/ldap.rb', line 70

def ssl
  !!datastore['SSL']
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:

Returns:

  • (Nil)

    This function does not return any data.

Raises:

  • (RuntimeError)

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



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/msf/core/exploit/remote/ldap.rb', line 235

def validate_bind_success!(ldap)
  if respond_to?(:session) && session
    vprint_good('Successfully bound to the LDAP server via existing SESSION!')
    return
  end

  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!')
    report_ldap_service
  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
    signing_statement = ''
    signing_statement = 'May require LDAP signing to be enabled (`set LDAP::Signing auto`). ' unless %w[ auto required ].include?(datastore['LDAP::Signing'])

    fail_with(Msf::Module::Failure::NoTarget, "Server requires a stronger form of authentication! #{signing_statement}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 (Rex::Proto::LDAP::Client::Filter) (defaults to: nil)

    A Rex::Proto::LDAP::Client::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.



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/msf/core/exploit/remote/ldap.rb', line 299

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 19
    fail_with(Msf::Module::Failure::BadConfig, 'A constraint on the operation was not satisfied')
  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