Skip to content

Commit 4941587

Browse files
committed
Expand on the matcher logic
This is necessary to account for effects from multiple ACEs.
1 parent 85324af commit 4941587

File tree

5 files changed

+262
-119
lines changed

5 files changed

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