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

Includes:
Msf::Exploit::Remote::LDAP, SecurityDescriptorMatcher, EntryCache
Defined in:
lib/msf/core/exploit/remote/ldap/active_directory/security_descriptor_matcher.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: SecurityDescriptorMatcher

Constant Summary collapse

LDAP_CAP_ACTIVE_DIRECTORY_OID =
'1.2.840.113556.1.4.800'.freeze
LDAP_SERVER_SD_FLAGS_OID =
'1.2.840.113556.1.4.801'.freeze
OWNER_SECURITY_INFORMATION =
0x1
GROUP_SECURITY_INFORMATION =
0x2
DACL_SECURITY_INFORMATION =
0x4
SACL_SECURITY_INFORMATION =
0x8

Constants included from SecurityDescriptorMatcher

SecurityDescriptorMatcher::CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT, SecurityDescriptorMatcher::CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT

Instance Method Summary collapse

Methods included from EntryCache

#ldap_entry_cache

Methods included from Msf::Exploit::Remote::LDAP

#get_connect_opts, #initialize, #ldap_connect, #ldap_escape_filter, #ldap_new, #ldap_open, #peer, #resolve_connect_opts, #rhost, #rport, #validate_bind_success!, #validate_query_result!

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

#initialize, #kerberos_storage_options, #kerberos_ticket_storage, store_ccache

Instance Method Details

#adds_build_ldap_sd_control(owner: true, group: true, dacl: true, sacl: false) ⇒ Object

Build a control blob that will fetch all security descriptor data but the SACL. This often enables reading a security descriptor’s DACL without the need for elevated permissions.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/msf/core/exploit/remote/ldap/active_directory.rb', line 44

def adds_build_ldap_sd_control(owner: true, group: true, dacl: true, sacl: false)
  # Set the value of LDAP_SERVER_SD_FLAGS_OID flag so everything but
  # the SACL flag is set, as we need administrative privileges to retrieve
  # the SACL from the ntSecurityDescriptor attribute on Windows AD LDAP servers.
  #
  # Note that without specifying the LDAP_SERVER_SD_FLAGS_OID control in this manner,
  # the LDAP searchRequest will default to trying to grab all possible attributes of
  # the ntSecurityDescriptor attribute, hence resulting in an attempt to retrieve the
  # SACL even if the user is not an administrative user.
  #
  # Now one may think that we would just get the rest of the data without the SACL field,
  # however in reality LDAP will cause that attribute to just be blanked out if a part of it
  # cannot be retrieved, so we just will get nothing for the ntSecurityDescriptor attribute
  # in these cases if the user doesn't have permissions to read the SACL.
  flags = 0
  flags |= OWNER_SECURITY_INFORMATION if owner
  flags |= GROUP_SECURITY_INFORMATION if group
  flags |= DACL_SECURITY_INFORMATION if dacl
  flags |= SACL_SECURITY_INFORMATION if sacl
  control_values = [flags].map(&:to_ber).to_ber_sequence.to_s.to_ber
  [LDAP_SERVER_SD_FLAGS_OID.to_ber, true.to_ber, control_values].to_ber_sequence
end

#adds_get_current_user(ldap) ⇒ Object

Get the LDAP object that describes the current user.

Parameters:



214
215
216
217
218
219
220
# File 'lib/msf/core/exploit/remote/ldap/active_directory.rb', line 214

def adds_get_current_user(ldap)
  whoami = @ldap_whoami = (@ldap_whoami || ldap.ldapwhoami.to_s)
  our_domain, _, our_username = whoami.delete_prefix('u:').partition('\\')
  # todo: this is probably going to have issues if our user is from a domain that the target server is not the
  # authority of
  adds_get_object_by_samaccountname(ldap, our_username)
end

#adds_get_domain_info(ldap) ⇒ Object

Get the AD DS domain info for the current server.

Parameters:



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

def adds_get_domain_info(ldap)
  domain_object = ldap.search(base: ldap.base_dn, filter: '(objectClass=domain)', return_result: true)&.first
  return nil unless domain_object

  ldap_entry_cache << domain_object
  domain_sid = Rex::Proto::MsDtyp::MsDtypSid.read(domain_object[:objectSid].first)

  root_dse = ldap.search(
    base: '',
    scope: Net::LDAP::SearchScope_BaseObject,
    attributes: %i[configurationNamingContext]
  )&.first
  return nil unless root_dse

  xrefs = ldap.search(
    base: root_dse[:configurationNamingContext].first,
    filter: "(&(objectCategory=crossref)(nETBIOSName=*)(nCName=#{ldap.base_dn}))"
  )
  return nil unless xrefs&.length == 1

  xref = xrefs.first
  ldap_entry_cache << xref

  {
    netbios_name: xref[:nETBIOSName].first.to_s,
    dns_name: xref[:dNSRoot].first.to_s,
    sid: domain_sid
  }
end

#adds_get_object_by_dn(ldap, object_dn) ⇒ Object

Obtain a particular entity by its distinguished name (DN).

Parameters:

  • ldap (Net::LDAP::Connection)

    The LDAP connection to use for querying.

  • object_dn (String)

    The full distinguished name of the object to retrieve.

Returns:

  • Returns nil when the object was not found.



157
158
159
160
161
162
163
164
165
166
# File 'lib/msf/core/exploit/remote/ldap/active_directory.rb', line 157

def adds_get_object_by_dn(ldap, object_dn)
  object = ldap_entry_cache.get_by_dn(object_dn)
  return object if object

  object = ldap.search(base: object_dn, controls: [adds_build_ldap_sd_control], scope: Net::LDAP::SearchScope_BaseObject)&.first
  validate_query_result!(ldap.get_operation_result.table)

  ldap_entry_cache << object if object
  object
end

#adds_get_object_by_samaccountname(ldap, object_samaccountname) ⇒ Object

Obtain a particular entity by its sAMAccountName.

Parameters:

  • ldap (Net::LDAP::Connection)

    The LDAP connection to use for querying.

  • object_samaccountname (String)

    The sAMAccountName of the object to retrieve.

Returns:

  • Returns nil when the object was not found.



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

def adds_get_object_by_samaccountname(ldap, object_samaccountname)
  object = ldap_entry_cache.get_by_samaccountname(object_samaccountname)
  return object if object

  filter = "(sAMAccountName=#{ldap_escape_filter(object_samaccountname)})"
  begin
    object = ldap.search(base: ldap.base_dn, controls: [adds_build_ldap_sd_control], filter: filter)&.first
  rescue Net::LDAP::Error => e
    elog('ldap search error for sAMAccountName', error: e)
    return nil
  end
  validate_query_result!(ldap.get_operation_result.table, filter)

  ldap_entry_cache << object if object
  object
end

#adds_get_object_by_sid(ldap, object_sid) ⇒ Object

Obtain a particular entity by its SID.

Parameters:

  • ldap (Net::LDAP::Connection)

    The LDAP connection to use for querying.

  • object_sid (String)

    The SID of the object to retrieve.

Returns:

  • Returns nil when the object was not found.



197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/msf/core/exploit/remote/ldap/active_directory.rb', line 197

def adds_get_object_by_sid(ldap, object_sid)
  object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid)
  object = ldap_entry_cache.get_by_sid(object_sid)
  return object if object

  filter = "(objectSID=#{ldap_escape_filter(object_sid.to_s)})"
  object = ldap.search(base: ldap.base_dn, controls: [adds_build_ldap_sd_control], filter: filter)&.first
  validate_query_result!(ldap.get_operation_result.table, filter)

  ldap_entry_cache << object if object
  object
end

#adds_obj_grants_permissions?(ldap, obj, matcher, test_sid: nil) ⇒ Boolean

Determine if a security descriptor will grant the permissions identified by matcher to the test_sid. For this to work, the authenticated user typically needs “Read permissions”, and “Read general information” from the advanced “Permission Entry” form in Active Directory. The more generic, “Read properties” permission will also do the trick.

Parameters:

  • ldap (Net::LDAP::Connection)

    The LDAP connection to use for querying.

  • obj (Net::LDAP::Entry)

    The LDAP object to test. The security descriptor will be taken from the nTSecurityDescriptor attribute.

  • matcher (#call)

    An object that will match ACEs that allow or deny the desired permissions.

  • test_sid (Rex::Proto::MsDtyp::MsDtypSid) (defaults to: nil)

    The SID to check for access.

Returns:

  • (Boolean)


343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/msf/core/exploit/remote/ldap/active_directory.rb', line 343

def adds_obj_grants_permissions?(ldap, obj, matcher, test_sid: nil)
  unless obj[:nTSecurityDescriptor].first
    raise RuntimeError.new('The nTSecurityDescriptor can not be read from the object.')
  end

  security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[:nTSecurityDescriptor].first)
  self_sid = nil
  if obj[:objectSid]&.first
    self_sid = Rex::Proto::MsDtyp::MsDtypSid.read(obj[:objectSid].first)
  end

  adds_sd_grants_permissions?(ldap, security_descriptor, matcher, test_sid: test_sid, self_sid: self_sid)
end

#adds_query_group_members(ldap, group_dn, base_dn: nil, inherited: true, object_class: nil) ⇒ Object

Query LDAP and obtain all members of a particular group. In this context, “members” are either users or groups.

Parameters:

  • ldap (Net::LDAP::Connection)

    The LDAP connection to use for querying.

  • group_dn (String)

    The DN of the group to obtain members for.

  • base_dn (String) (defaults to: nil)

    An optional base search DN.

  • inherited (Boolean) (defaults to: true)

    Whether or not to include entities that are members by inheritance.

  • object_class (String) (defaults to: nil)

    An optional object class for filtering. This is typically either ‘user’ or ‘group’.



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

def adds_query_group_members(ldap, group_dn, base_dn: nil, inherited: true, object_class: nil)
  return enum_for(:adds_query_group_members, ldap, group_dn, base_dn: base_dn, inherited: inherited, object_class: object_class) unless block_given?
  results = 0

  member_filter = "memberOf#{inherited ? ':1.2.840.113556.1.4.1941:' : ''}=#{ldap_escape_filter(group_dn)}"

  # Get the member's primaryGroupID
  group = adds_get_object_by_dn(ldap, group_dn)
  if group && group[:objectSID]
    group_sid = Rex::Proto::MsDtyp::MsDtypSid.read(group[:objectSID].first)
    # if we have a group RID, filter on that when the object has it as it's primaryGroupId to include those groups too
    member_filter = "|(#{member_filter})(primaryGroupId=#{group_sid.rid})"
  end

  filters = []
  filters << "objectClass=#{ldap_escape_filter(object_class)}" if object_class
  filters << member_filter

  ldap.search(
    base: base_dn || ldap.base_dn,
    controls: [adds_build_ldap_sd_control],
    filter: "(&#{filters.map { "(#{_1})" }.join})",
    return_result: false # make sure we're streaming because this could be a lot of data
  ) do |ldap_entry|
    yield ldap_entry
    results += 1
  end

  unless ldap.get_operation_result.code == 0
    raise "LDAP Error: #{ldap.get_operation_result.message}"
  end

  results
end

#adds_query_member_groups(ldap, member_dn, base_dn: nil, inherited: true) ⇒ Object

Query LDAP and obtain all groups a particular entity is a member of. In this context, “members” are either users or groups.

Parameters:

  • ldap (Net::LDAP::Connection)

    The LDAP connection to use for querying.

  • member_dn (String)

    The DN of the member to obtain groups for.

  • base_dn (String) (defaults to: nil)

    An optional base search DN.

  • inherited (Boolean) (defaults to: true)

    Whether or not to include groups that are inherited.



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

def adds_query_member_groups(ldap, member_dn, base_dn: nil, inherited: true)
  return enum_for(:adds_query_member_groups, ldap, member_dn, base_dn: base_dn, inherited: inherited) unless block_given?
  results = 0

  # Get the member's primaryGroupId
  member = adds_get_object_by_dn(ldap, member_dn)
  if member && member[:objectSid] && member[:primaryGroupId] && !member[:primaryGroupId].empty?
    # if it's found, calculate the SID of the primary group and query it, the primary group is typically 'Domain Users'
    # and is *not* included in the member query
    member_sid = Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first)
    primary_group_sid = "#{member_sid.to_s.rpartition('-').first}-#{member[:primaryGroupId].first}"
    primary_group = adds_get_object_by_sid(ldap, primary_group_sid)
    yield primary_group if primary_group
  end

  filters = []
  filters << "objectClass=group"
  filters << "member#{inherited ? ':1.2.840.113556.1.4.1941:' : ''}=#{ldap_escape_filter(member_dn)}"

  ldap.search(
    base: base_dn || ldap.base_dn,
    controls: [adds_build_ldap_sd_control],
    filter: "(&#{filters.map { "(#{_1})" }.join})",
    return_result: false
  ) do |ldap_entry|
    yield ldap_entry
    results += 1
  end

  unless ldap.get_operation_result.code == 0
    raise "LDAP Error: #{ldap.get_operation_result.message}"
  end

  results
end

#adds_sd_grants_permissions?(ldap, security_descriptor, matcher, test_sid: nil, self_sid: nil) ⇒ Boolean

Determine if a security descriptor will grant the permissions identified by matcher to the test_sid.

Parameters:

Returns:

  • (Boolean)


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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/msf/core/exploit/remote/ldap/active_directory.rb', line 267

def adds_sd_grants_permissions?(ldap, security_descriptor, matcher, test_sid: nil, self_sid: nil)
  unless test_sid
    current_user = adds_get_current_user(ldap)
    raise RuntimeError.new('No SID was specified and the current user could not be identified.') unless current_user

    test_sid = Rex::Proto::MsDtyp::MsDtypSid.read(current_user[:objectSid].first)
  end

  test_member_sids = nil

  dacl_aces = []
  # because deny entries take precedence, process them first
  dacl_aces += security_descriptor.dacl.aces.select { |ace| Rex::Proto::MsDtyp::MsDtypAceType.deny?(ace.header.ace_type) }
  dacl_aces += security_descriptor.dacl.aces.select { |ace| Rex::Proto::MsDtyp::MsDtypAceType.allow?(ace.header.ace_type) }

  dacl_aces.each do |ace|
    # Uncomment this if you need to debug ACE evaluation
    # ldap_object = adds_get_object_by_sid(ldap, ace.body.sid)
    # $stderr.puts  "ACE:"
    # $stderr.puts  "  Type:        #{Rex::Proto::MsDtyp::MsDtypAceType.name(ace.header.ace_type)}"
    # $stderr.puts  "  Permissions: #{ace.body.access_mask.permissions.map(&:to_s).join(', ')}"
    # $stderr.write "  SID:         #{ace.body.sid}"
    # $stderr.puts (ldap_object && ldap_object[:sAMAccountName].first) ? " (#{ldap_object[:sAMAccountName].first})" : ""
    # $stderr.puts "  Object:      #{ace.body.object_type}" if Rex::Proto::MsDtyp::MsDtypAceType.has_object?(ace.header.ace_type)

    next if matcher.ignore_ace?(ace)

    case ace.body.sid
    when Rex::Proto::Secauthz::WellKnownSids::SECURITY_WORLD_SID
      matcher.apply_ace!(ace)
    when Rex::Proto::Secauthz::WellKnownSids::SECURITY_PRINCIPAL_SELF_SID
      matcher.apply_ace!(ace) if self_sid == test_sid
    when Rex::Proto::Secauthz::WellKnownSids::SECURITY_CREATOR_OWNER_SID
      matcher.apply_ace!(ace) if security_descriptor.owner_sid == test_sid
    when Rex::Proto::Secauthz::WellKnownSids::SECURITY_CREATOR_GROUP_SID
      matcher.apply_ace!(ace) if security_descriptor.group_sid == test_sid
    when test_sid
      matcher.apply_ace!(ace)
    else
      ldap_object = adds_get_object_by_sid(ldap, ace.body.sid)
      next unless ldap_object && ldap_object[:objectClass].include?('group')

      member_sids = adds_query_group_members(ldap, ldap_object.dn, inherited: false).map { |member| Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first) }
      if member_sids.include?(test_sid)
        matcher.apply_ace!(ace)
        next
      end

      if test_member_sids.nil?
        test_obj = adds_get_object_by_sid(ldap, test_sid)
        test_member_sids = adds_query_member_groups(ldap, test_obj.dn, inherited: true).map { |member| Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first) }.to_set
        if test_obj[:objectClass].include?('user') && test_sid.rid != Rex::Proto::Secauthz::WellKnownSids::DOMAIN_USER_RID_GUEST
          test_member_sids << Rex::Proto::Secauthz::WellKnownSids::SECURITY_AUTHENTICATED_USER_SID
          test_member_sids << Rex::Proto::Secauthz::WellKnownSids::DOMAIN_ALIAS_SID_USERS
        end
      end

      matcher.apply_ace!(ace) if member_sids.any? { |member_sid| test_member_sids.include?(member_sid) }
    end

    break if matcher.satisfied?
  end

  matcher.matches?
end

#is_active_directory?(ldap) ⇒ Boolean

Query the remote server via the provided LDAP connection to determine if it’s an Active Directory LDAP server. More specifically, this ensures that it reports active directory capabilities and the whoami extension.

Parameters:

  • Net::LDAP::Connection

    ldap_connection

Returns:

  • (Boolean)


25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/msf/core/exploit/remote/ldap/active_directory.rb', line 25

def is_active_directory?(ldap)
  root_dse = ldap.search(
    ignore_server_caps: true,
    base: '',
    scope: Net::LDAP::SearchScope_BaseObject,
    attributes: %i[ supportedCapabilities supportedExtension ]
  )&.first

  return false unless root_dse[:supportedCapabilities].map(&:to_s).include?(LDAP_CAP_ACTIVE_DIRECTORY_OID)

  return false unless root_dse[:supportedExtension].include?(Net::LDAP::WhoamiOid)

  true
end