Skip to content

Commit be40c05

Browse files
committed
Initial implementation of #adds_sd_grants_permissions?
1 parent e4324e6 commit be40c05

File tree

1 file changed

+88
-2
lines changed

1 file changed

+88
-2
lines changed

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

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ module ActiveDirectory
99
include Msf::Exploit::Remote::LDAP
1010

1111
LDAP_CAP_ACTIVE_DIRECTORY_OID = '1.2.840.113556.1.4.800'.freeze
12+
LDAP_SERVER_SD_FLAGS_OID = '1.2.840.113556.1.4.801'.freeze
13+
OWNER_SECURITY_INFORMATION = 0x1
14+
GROUP_SECURITY_INFORMATION = 0x2
15+
DACL_SECURITY_INFORMATION = 0x4
16+
SACL_SECURITY_INFORMATION = 0x8
1217

1318
# Query the remote server via the provided LDAP connection to determine if it's an Active Directory LDAP server.
1419
# More specifically, this ensures that it reports active directory capabilities and the whoami extension.
@@ -30,6 +35,25 @@ def is_active_directory?(ldap)
3035
true
3136
end
3237

38+
def adds_build_ldap_sd_control
39+
# Set the value of LDAP_SERVER_SD_FLAGS_OID flag so everything but
40+
# the SACL flag is set, as we need administrative privileges to retrieve
41+
# the SACL from the ntSecurityDescriptor attribute on Windows AD LDAP servers.
42+
#
43+
# Note that without specifying the LDAP_SERVER_SD_FLAGS_OID control in this manner,
44+
# the LDAP searchRequest will default to trying to grab all possible attributes of
45+
# the ntSecurityDescriptor attribute, hence resulting in an attempt to retrieve the
46+
# SACL even if the user is not an administrative user.
47+
#
48+
# Now one may think that we would just get the rest of the data without the SACL field,
49+
# however in reality LDAP will cause that attribute to just be blanked out if a part of it
50+
# cannot be retrieved, so we just will get nothing for the ntSecurityDescriptor attribute
51+
# in these cases if the user doesn't have permissions to read the SACL.
52+
all_but_sacl_flag = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
53+
control_values = [all_but_sacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber
54+
[LDAP_SERVER_SD_FLAGS_OID.to_ber, true.to_ber, control_values].to_ber_sequence
55+
end
56+
3357
def adds_query_group_members(ldap, group_dn, base_dn: nil, inherited: true, object_class: nil)
3458
return enum_for(:adds_query_group_members, ldap, group_dn, base_dn: base_dn, inherited: inherited, object_class: object_class) unless block_given?
3559
results = 0
@@ -50,6 +74,7 @@ def adds_query_group_members(ldap, group_dn, base_dn: nil, inherited: true, obje
5074

5175
ldap.search(
5276
base: base_dn || ldap.base_dn,
77+
controls: [adds_build_ldap_sd_control],
5378
filter: "(&#{filters.map { "(#{_1})" }.join})",
5479
return_result: false # make sure we're streaming because this could be a lot of data
5580
) do |ldap_entry|
@@ -85,6 +110,7 @@ def adds_query_member_groups(ldap, member_dn, base_dn: nil, inherited: true)
85110

86111
ldap.search(
87112
base: base_dn || ldap.base_dn,
113+
controls: [adds_build_ldap_sd_control],
88114
filter: "(&#{filters.map { "(#{_1})" }.join})",
89115
return_result: false
90116
) do |ldap_entry|
@@ -104,26 +130,86 @@ def adds_get_object_by_dn(ldap, object_dn)
104130
object = @ldap_objects.find { |o| o[:dN]&.first == object_dn }
105131
return object if object
106132

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

110136
@ldap_objects << object if object
111137
object
112138
end
113139

140+
def adds_get_object_by_samaccountname(ldap, object_samaccountname)
141+
@ldap_objects ||= []
142+
object = @ldap_objects.find { |o| o[:sAMAccountName]&.first == object_samaccountname }
143+
return object if object
144+
145+
filter = "(sAMAccountName=#{ldap_escape_filter(object_samaccountname)})"
146+
object = ldap.search(base: ldap.base_dn, controls: [adds_build_ldap_sd_control], filter: filter)&.first
147+
validate_query_result!(ldap.get_operation_result.table, filter)
148+
149+
@ldap_objects << object if object
150+
object
151+
end
152+
114153
def adds_get_object_by_sid(ldap, object_sid)
115154
@ldap_objects ||= []
116155
object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid)
117156
object = @ldap_objects.find { |o| o[:objectSid]&.first == object_sid.to_binary_s }
118157
return object if object
119158

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

124163
@ldap_objects << object if object
125164
object
126165
end
166+
167+
def adds_get_current_user(ldap)
168+
our_domain, _, our_username = ldap.ldapwhoami.to_s.delete_prefix('u:').partition('\\')
169+
# todo: this is probably going to have issues if our user is from a domain that the target server is not the
170+
# authority of
171+
adds_get_object_by_samaccountname(ldap, our_username)
172+
end
173+
174+
def adds_sd_grants_permissions?(ldap, security_descriptor, matcher, test_sid: nil)
175+
unless test_sid
176+
current_user = adds_get_current_user(ldap)
177+
test_sid = Rex::Proto::MsDtyp::MsDtypSid.read(current_user[:objectSid].first)
178+
end
179+
180+
# todo: process special SIDs, e.g. SELF, Everyone
181+
182+
test_member_sids = nil
183+
security_descriptor.dacl.aces.each do |ace|
184+
# only processing simple allow and deny types right now
185+
if ace.header.ace_type == Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_ALLOWED_ACE_TYPE
186+
eval_result = true
187+
elsif ace.header.ace_type == Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_DENIED_ACE_TYPE
188+
eval_result = false
189+
else
190+
next # skip ACEs that are neither allow or deny
191+
end
192+
193+
next unless matcher.call(ace)
194+
195+
return eval_result if ace.body.sid == test_sid
196+
197+
ldap_object = adds_get_object_by_sid(ldap, ace.body.sid)
198+
next unless ldap_object && ldap_object[:objectClass].include?('group')
199+
200+
member_sids = adds_query_group_members(ldap, ldap_object[:dN].first, inherited: false).map { |member| Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first) }
201+
return eval_result if member_sids.include?(test_sid)
202+
203+
if test_member_sids.nil?
204+
test_obj = adds_get_object_by_sid(ldap, test_sid)
205+
test_member_sids = adds_query_member_groups(ldap, test_obj[:dN].first, inherited: true).map { |member| Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first) }
206+
end
207+
208+
return eval_result if member_sids.any? { |member_sid| test_member_sids.include?(member_sid) }
209+
end
210+
211+
false
212+
end
127213
end
128214
end
129215
end

0 commit comments

Comments
 (0)