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

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

Overview

This module exposes methods for querying a remote LDAP service

Defined Under Namespace

Modules: Queries, Server

Instance Method Summary collapse

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

#ldap_connect_opts

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.



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/msf/core/exploit/remote/ldap.rb', line 201

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

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



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/msf/core/exploit/remote/ldap.rb', line 76

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

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



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/msf/core/exploit/remote/ldap.rb', line 176

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.



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

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

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:



97
98
99
# File 'lib/msf/core/exploit/remote/ldap.rb', line 97

def ldap_connect(opts = {}, &block)
  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.



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

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

#ldap_open(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:

  • connect_opts (Hash)

    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


112
113
114
115
# File 'lib/msf/core/exploit/remote/ldap.rb', line 112

def ldap_open(connect_opts, &block)
  opts = resolve_connect_opts(connect_opts)
  Net::LDAP.open(opts, &block)
end

#peerString

Return the peer as a host:port formatted string.

Returns:

  • (String)

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



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

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

#resolve_connect_opts(connect_opts) ⇒ Object



118
119
120
121
122
123
124
125
126
127
# File 'lib/msf/core/exploit/remote/ldap.rb', line 118

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.



52
53
54
# File 'lib/msf/core/exploit/remote/ldap.rb', line 52

def rhost
  datastore['RHOST']
end

#rportString

Alias to return the RPORT datastore option.

Returns:

  • (String)

    The current value of RPORT in the datastore.



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

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.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/msf/core/exploit/remote/ldap.rb', line 233

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.



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
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
# File 'lib/msf/core/exploit/remote/ldap.rb', line 273

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