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/error.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 Classes: Error

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

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



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

def get_connect_opts
  opts = {
    username: datastore['USERNAME'],
    password: datastore['PASSWORD'],
    domain: datastore['DOMAIN'],
    base: datastore['BASE_DN'],
    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,
    kerberos_ticket_storage: kerberos_ticket_storage
  }
  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: datastore['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.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 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]),
      OptEnum.new('LDAP::Signing', [true, 'Use signed and sealed (encrypted) LDAP', 'auto', %w[ disabled auto required ]])
    ]
  )
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:



124
125
126
# File 'lib/msf/core/exploit/remote/ldap.rb', line 124

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.



320
321
322
# File 'lib/msf/core/exploit/remote/ldap.rb', line 320

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:



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

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


141
142
143
144
145
146
147
148
# File 'lib/msf/core/exploit/remote/ldap.rb', line 141

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.



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

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

#resolve_connect_opts(connect_opts) ⇒ Object



151
152
153
154
155
156
157
158
159
160
# File 'lib/msf/core/exploit/remote/ldap.rb', line 151

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.



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

def rhost
  datastore['RHOST']
end

#rportString

Alias to return the RPORT datastore option.

Returns:

  • (String)

    The current value of RPORT in the datastore.



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

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:

Returns:

  • (Nil)

    This function does not return any data.

Raises:

  • (RuntimeError)

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



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/msf/core/exploit/remote/ldap.rb', line 215

def validate_bind_success!(ldap)
  if defined?(: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!')
  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.



263
264
265
266
267
268
269
270
271
272
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
# File 'lib/msf/core/exploit/remote/ldap.rb', line 263

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