Skip to content

Commit 3a5b275

Browse files
committed
Expand on the matcher logic
This is necessary to account for effects from multiple ACEs.
1 parent cf48211 commit 3a5b275

File tree

5 files changed

+270
-119
lines changed

5 files changed

+270
-119
lines changed

lib/msf/core/exploit/remote/ldap/active_directory.rb

Lines changed: 21 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Exploit::Remote::LDAP
88
module ActiveDirectory
99
include Msf::Exploit::Remote::LDAP
1010
include Msf::Exploit::Remote::LDAP::EntryCache
11+
include Msf::Exploit::Remote::LDAP::ActiveDirectory::SecurityDescriptorMatcher
1112

1213
LDAP_CAP_ACTIVE_DIRECTORY_OID = '1.2.840.113556.1.4.800'.freeze
1314
LDAP_SERVER_SD_FLAGS_OID = '1.2.840.113556.1.4.801'.freeze
@@ -16,56 +17,6 @@ module ActiveDirectory
1617
DACL_SECURITY_INFORMATION = 0x4
1718
SACL_SECURITY_INFORMATION = 0x8
1819

19-
# Match an ACE when *any* of the specified permissions are allowed or *all* of the specified permissions are
20-
# denied.
21-
class ACEMatcherAllowAll
22-
attr_reader :permissions
23-
24-
def initialize(permissions)
25-
@permissions = Array.wrap(permissions)
26-
end
27-
28-
def call(ace)
29-
target_permissions = ace.body.access_mask.permissions
30-
31-
if target_permissions.empty?
32-
return false
33-
elsif Rex::Proto::MsDtyp::MsDtypAceType.allowed?(ace.header.ace_type)
34-
return @permissions.all? { |perm| target_permissions.include?(perm) }
35-
elsif Rex::Proto::MsDtyp::MsDtypAceType.denied?(ace.header.ace_type)
36-
# if any target permission is denied, then all of them can't be allowed
37-
return @permissions.any? { |perm| target_permissions.include?(perm) }
38-
end
39-
40-
false
41-
end
42-
end
43-
44-
# Match an ACE when *all* of the specified permissions are allowed or *any* of the specified permissions are
45-
# denied.
46-
class ACEMatcherAllowAny
47-
attr_reader :permissions
48-
49-
def initialize(permissions)
50-
@permissions = Array.wrap(permissions)
51-
end
52-
53-
def call(ace)
54-
target_permissions = ace.body.access_mask.permissions
55-
56-
if target_permissions.empty?
57-
return false
58-
elsif Rex::Proto::MsDtyp::MsDtypAceType.allowed?(ace.header.ace_type)
59-
return @permissions.any? { |perm| target_permissions.include?(perm) }
60-
elsif Rex::Proto::MsDtyp::MsDtypAceType.denied?(ace.header.ace_type)
61-
# if all target permissions are denied, then none of them can be allowed
62-
return @permissions.all? { |perm| target_permissions.include?(perm) }
63-
end
64-
65-
false
66-
end
67-
end
68-
6920
# Query the remote server via the provided LDAP connection to determine if it's an Active Directory LDAP server.
7021
# More specifically, this ensures that it reports active directory capabilities and the whoami extension.
7122
#
@@ -298,7 +249,7 @@ def adds_get_domain_info(ldap)
298249
# @param [Net::LDAP::Connection] ldap The LDAP connection to use for querying.
299250
# @param [Rex::Proto::MsDtyp::MsDtypSecurityDescriptor] security_descriptor The security descriptor object to
300251
# evaluate.
301-
# @param [#call] matcher An object that will match ACEs that allow or deny the desired permissions.
252+
# @param [SecurityDescriptorMatcher::Base] matcher An object that will match ACEs that allow or deny the desired permissions.
302253
# @param [Rex::Proto::MsDtyp::MsDtypSid] test_sid The SID to check for access.
303254
# @param [Rex::Proto::MsDtyp::MsDtypSid] self_sid The SID of the object who owns the security_descriptor. This is
304255
# typically the objectSid LDAP attribute and is used when the security descriptor references the special 'SELF'
@@ -310,51 +261,45 @@ def adds_sd_grants_permissions?(ldap, security_descriptor, matcher, test_sid: ni
310261
end
311262

312263
test_member_sids = nil
313-
security_descriptor.dacl.aces.each do |ace|
314-
# only processing simple allow and deny types right now
315-
case ace.header.ace_type
316-
when Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_ALLOWED_ACE_TYPE
317-
eval_result = true
318-
when Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_ALLOWED_OBJECT_ACE_TYPE
319-
eval_result = true
320-
when Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_DENIED_ACE_TYPE
321-
eval_result = false
322-
when Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_DENIED_OBJECT_ACE_TYPE
323-
eval_result = false
324-
else
325-
next # skip ACEs that are neither allow or deny
326-
end
327264

328-
next unless matcher.call(ace)
265+
dacl_aces = []
266+
# because deny entries take precedence, process them first
267+
dacl_aces += security_descriptor.dacl.aces.select { |ace| Rex::Proto::MsDtyp::MsDtypAceType.deny?(ace.header.ace_type) }
268+
dacl_aces += security_descriptor.dacl.aces.select { |ace| Rex::Proto::MsDtyp::MsDtypAceType.allow?(ace.header.ace_type) }
269+
270+
dacl_aces.each do |ace|
271+
next if matcher.ignore_ace?(ace)
329272

330273
case ace.body.sid
331274
when Rex::Proto::Secauthz::WellKnownSids::SECURITY_WORLD_SID
332-
return eval_result
275+
matcher.apply_ace!(ace)
333276
when Rex::Proto::Secauthz::WellKnownSids::SECURITY_PRINCIPAL_SELF_SID
334-
return eval_result if self_sid == test_sid
277+
matcher.apply_ace!(ace) if self_sid == test_sid
335278
when Rex::Proto::Secauthz::WellKnownSids::SECURITY_CREATOR_OWNER_SID
336-
return eval_result if security_descriptor.owner_sid == test_sid
279+
matcher.apply_ace!(ace) if security_descriptor.owner_sid == test_sid
337280
when Rex::Proto::Secauthz::WellKnownSids::SECURITY_CREATOR_GROUP_SID
338-
return eval_result if security_descriptor.group_sid == test_sid
281+
matcher.apply_ace!(ace) if security_descriptor.group_sid == test_sid
339282
when test_sid
340-
return eval_result
283+
matcher.apply_ace!(ace)
341284
else
342285
ldap_object = adds_get_object_by_sid(ldap, ace.body.sid)
343286
next unless ldap_object && ldap_object[:objectClass].include?('group')
344287

345-
member_sids = adds_query_group_members(ldap, ldap_object[:dN].first, inherited: false).map { |member| Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first) }
346-
return eval_result if member_sids.include?(test_sid)
288+
member_sids = adds_query_group_members(ldap, ldap_object.dn, inherited: false).map { |member| Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first) }
289+
matcher.apply_ace!(ace) if member_sids.include?(test_sid)
347290

348291
if test_member_sids.nil?
349292
test_obj = adds_get_object_by_sid(ldap, test_sid)
350-
test_member_sids = adds_query_member_groups(ldap, test_obj[:dN].first, inherited: true).map { |member| Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first) }
293+
test_member_sids = adds_query_member_groups(ldap, test_obj.dn, inherited: true).map { |member| Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first) }
351294
end
352295

353-
return eval_result if member_sids.any? { |member_sid| test_member_sids.include?(member_sid) }
296+
matcher.apply_ace!(ace) if member_sids.any? { |member_sid| test_member_sids.include?(member_sid) }
354297
end
298+
299+
break if matcher.satisfied?
355300
end
356301

357-
false
302+
matcher.matches?
358303
end
359304

360305
# Determine if a security descriptor will grant the permissions identified by *matcher* to the
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
module Msf
2+
###
3+
#
4+
# This module exposes methods for querying a remote LDAP service
5+
#
6+
###
7+
module Exploit::Remote::LDAP::ActiveDirectory
8+
9+
module SecurityDescriptorMatcher
10+
CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT = '0e10c968-78fb-11d2-90d4-00c04f79dc55'.freeze
11+
CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT = 'a05b8cc2-17bc-4802-a710-e7c15ab866a2'.freeze
12+
13+
# This is a Base matcher class that can be used while analyzing security descriptors. It abstracts away the
14+
# checking of the permissions but relies on an external source to identify when particular ACEs will be in effect
15+
# based on a target principal SID.
16+
class Base
17+
# Check the ACE and determine if it should be ignored while processing. This allows processing to skip querying
18+
# the LDAP server when it's known that the ACE is irrelevant.
19+
#
20+
# @param [Rex::Proto::MsDtyp::MsDtypAce] ace The ace to check.
21+
def ignore_ace?(ace)
22+
false
23+
end
24+
25+
# Apply the specified ACE to the internal state of the matcher because it will be applied in the hypothetical
26+
# access operation that is being analyzed.
27+
#
28+
# @param [Rex::Proto::MsDtyp::MsDtypAce] ace The ace to apply.
29+
# @rtype Nil
30+
def apply_ace!(ace)
31+
nil
32+
end
33+
34+
# The matcher is satisfied when it has all the information it needs from previous calls to #apply_ace! to make
35+
# a determination with #matches?.
36+
def satisfied?
37+
false
38+
end
39+
40+
# The matcher matches when it is satisfied and confident that the desired affect will be applied in the
41+
# hypothetical access operation.
42+
def matches?
43+
false
44+
end
45+
end
46+
47+
# A general purpose Security Descriptor matcher for a single permission.
48+
class Allow < Base
49+
attr_reader :permissions
50+
51+
# @param [Symbol] permission The abbreviated permission name e.g. :WP.
52+
# @param [String] object_id An optional object GUID to use when matching the permission.
53+
def initialize(permission, object_id: nil)
54+
@permission = permission
55+
@object_id = object_id
56+
@result = nil
57+
end
58+
59+
def ignore_ace?(ace)
60+
# ignore anything that's not an allow or deny ACE because those are the only two that will alter the outcome
61+
return true unless Rex::Proto::MsDtyp::MsDtypAceType.allow?(ace.header.ace_type) || Rex::Proto::MsDtyp::MsDtypAceType.deny?(ace.header.ace_type)
62+
63+
if Rex::Proto::MsDtyp::MsDtypAceType.has_object?(ace.header.ace_type)
64+
return true if ace.body.object_type != @object_id
65+
else
66+
return true if @object_id
67+
end
68+
69+
!ace.body.access_mask.permissions.include?(@permission)
70+
end
71+
72+
def apply_ace!(ace)
73+
return if ignore_ace?(ace)
74+
75+
@result = ace.header.ace_type
76+
end
77+
78+
def satisfied?
79+
# A matcher is satisfied when there's nothing left for it to check.
80+
!@result.nil?
81+
end
82+
83+
def matches?
84+
# This is named matches? instead of allow? so that other matchers can be made in the future
85+
# to match on the desired outcome including audit and alarm events. We are affirming that the
86+
# security descriptor will apply the desired affect which in this case is to allow access.
87+
satisfied? && Rex::Proto::MsDtyp::MsDtypAceType.allow?(@result)
88+
end
89+
90+
# Build a matcher that will check for any of the specified permissions.
91+
#
92+
# @param [Array<Symbol>] The permissions to check for in their 2 letter abbreviated format, e.g. WP.
93+
# @param [String,Nil] An optional object ID that will be used for matching all the permissions.
94+
# @rtype MultipleAny
95+
def self.any(permissions, object_id: nil)
96+
permissions = Array.wrap(permissions)
97+
MultipleAll.new(permissions.map { |permission| new(permission, object_id: object_id) })
98+
end
99+
100+
# Build a matcher that will check for all of the specified permissions.
101+
#
102+
# @param [Array<Symbol>] The permissions to check for in their 2 letter abbreviated format, e.g. WP.
103+
# @param [String,Nil] An optional object ID that will be used for matching all the permissions.
104+
# @rtype MultipleAll
105+
def self.all(permissions, object_id: nil)
106+
permissions = Array.wrap(permissions)
107+
MultipleAll.new(permissions.map { |permission| new(permission, object_id: object_id) })
108+
end
109+
110+
# Build a matcher that will check for a certificate's enrollment permission.
111+
def self.certificate_enrollment
112+
new(:CR, object_id: CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT)
113+
end
114+
end
115+
116+
# A compound matcher that will match when any of the sub-matchers match.
117+
class MultipleAny < Base
118+
attr_reader :matchers
119+
120+
def initialize(matchers)
121+
@matchers = Array.wrap(matchers)
122+
end
123+
124+
def ignore_ace?(ace)
125+
@matchers.all? { |matcher| matcher.ignore_ace?(ace) }
126+
end
127+
128+
def apply_ace!(ace)
129+
@matchers.each do |matcher|
130+
next if matcher.ignore_ace?(ace)
131+
132+
matcher.apply_ace!(ace)
133+
end
134+
end
135+
136+
def satisfied?
137+
@matchers.any? { |matcher| matcher.satisfied? }
138+
end
139+
140+
def matches?
141+
@matchers.any? { |matcher| matcher.matches? }
142+
end
143+
end
144+
145+
# A compound matcher that will match when all of the sub-matchers match.
146+
class MultipleAll < MultipleAny
147+
def satisfied?
148+
@matchers.all? { |matcher| matcher.satisfied? }
149+
end
150+
151+
def matches?
152+
@matchers.all? { |matcher| matcher.matches? }
153+
end
154+
end
155+
end
156+
end
157+
end

lib/rex/proto/ms_dtyp.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ def self.alarm?(type)
340340
].include? type
341341
end
342342

343-
def self.allowed?(type)
343+
def self.allow?(type)
344344
[
345345
ACCESS_ALLOWED_ACE_TYPE,
346346
ACCESS_ALLOWED_COMPOUND_ACE_TYPE,
@@ -359,14 +359,27 @@ def self.audit?(type)
359359
].include? type
360360
end
361361

362-
def self.denied?(type)
362+
def self.deny?(type)
363363
[
364364
ACCESS_DENIED_ACE_TYPE,
365365
ACCESS_DENIED_OBJECT_ACE_TYPE,
366366
ACCESS_DENIED_CALLBACK_ACE_TYPE,
367367
ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE,
368368
].include? type
369369
end
370+
371+
def self.has_object?(type)
372+
[
373+
ACCESS_ALLOWED_OBJECT_ACE_TYPE,
374+
ACCESS_DENIED_OBJECT_ACE_TYPE,
375+
SYSTEM_AUDIT_OBJECT_ACE_TYPE,
376+
SYSTEM_ALARM_OBJECT_ACE_TYPE,
377+
ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE,
378+
ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE,
379+
SYSTEM_AUDIT_CALLBACK_OBJECT_ACE_TYPE,
380+
SYSTEM_ALARM_CALLBACK_OBJECT_ACE_TYPE
381+
].include? type
382+
end
370383
end
371384

372385
# [2.4.4.1 ACE_HEADER](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/628ebb1d-c509-4ea0-a10f-77ef97ca4586)

modules/auxiliary/admin/ldap/rbcd.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def check
115115
return Exploit::CheckCode::Unknown('Failed to find the specified object.')
116116
end
117117

118-
unless adds_obj_grants_permissions?(@ldap, obj, ACEMatcherAllowAll.new(%i[RP WP]))
118+
unless adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.all(%i[RP WP]))
119119
return Exploit::CheckCode::Safe('The object can not be written to.')
120120
end
121121

0 commit comments

Comments
 (0)