Skip to content

Commit 79d67dd

Browse files
authored
Merge pull request #20345 from zeroSteiner/feat/lib/ldap-adds/1
Add an Active Directory LDAP Mixin
2 parents 65124d0 + 2ab90df commit 79d67dd

File tree

13 files changed

+1024
-207
lines changed

13 files changed

+1024
-207
lines changed

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ PATH
4141
irb
4242
jsobfu
4343
json
44+
lru_redux
4445
metasm
4546
metasploit-concern
4647
metasploit-credential
@@ -307,6 +308,7 @@ GEM
307308
loofah (2.24.0)
308309
crass (~> 1.0.2)
309310
nokogiri (>= 1.12.0)
311+
lru_redux (1.1.0)
310312
memory_profiler (1.1.0)
311313
metasm (1.0.5)
312314
metasploit-concern (5.0.4)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def ldap.use_connection(args)
213213
# bind request failed.
214214
# @return [Nil] This function does not return any data.
215215
def validate_bind_success!(ldap)
216-
if defined?(:session) && session
216+
if respond_to?(:session) && session
217217
vprint_good('Successfully bound to the LDAP server via existing SESSION!')
218218
return
219219
end

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

Lines changed: 358 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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 permissions in a single ACE. You typically want to use this to
48+
# check for a single permission.
49+
class Allow < Base
50+
attr_reader :permissions
51+
52+
# @param [Symbol, Array<Symbol>] permissions The abbreviated permission names e.g. :WP.
53+
# @param [String] object_id An optional object GUID to use when matching the permission.
54+
def initialize(permissions, object_id: nil)
55+
@permissions = Array.wrap(permissions)
56+
@object_id = object_id
57+
@result = nil
58+
end
59+
60+
def ignore_ace?(ace)
61+
# ignore anything that's not an allow or deny ACE because those are the only two that will alter the outcome
62+
return true unless Rex::Proto::MsDtyp::MsDtypAceType.allow?(ace.header.ace_type) || Rex::Proto::MsDtyp::MsDtypAceType.deny?(ace.header.ace_type)
63+
64+
if Rex::Proto::MsDtyp::MsDtypAceType.has_object?(ace.header.ace_type) && ace.body.flags.ace_object_type_present == 1
65+
return true if ace.body.object_type != @object_id
66+
else
67+
return true if @object_id
68+
end
69+
70+
ace_permissions = ace.body.access_mask.permissions
71+
!@permissions.all? { |perm| ace_permissions.include?(perm) }
72+
end
73+
74+
def apply_ace!(ace)
75+
return if ignore_ace?(ace)
76+
77+
@result = ace.header.ace_type
78+
79+
nil
80+
end
81+
82+
def satisfied?
83+
# A matcher is satisfied when there's nothing left for it to check.
84+
!@result.nil?
85+
end
86+
87+
def matches?
88+
# This is named matches? instead of allow? so that other matchers can be made in the future
89+
# to match on the desired outcome including audit and alarm events. We are affirming that the
90+
# security descriptor will apply the desired affect which in this case is to allow access.
91+
satisfied? && Rex::Proto::MsDtyp::MsDtypAceType.allow?(@result)
92+
end
93+
94+
# Build a matcher that will check for any of the specified permissions.
95+
#
96+
# @param [Array<Symbol>] The permissions to check for in their 2 letter abbreviated format, e.g. WP.
97+
# @param [String,Nil] An optional object ID that will be used for matching all the permissions.
98+
# @rtype MultipleAny
99+
def self.any(permissions, object_id: nil)
100+
permissions = Array.wrap(permissions)
101+
MultipleAll.new(permissions.map { |permission| new(permission, object_id: object_id) })
102+
end
103+
104+
# Build a matcher that will check for all of the specified permissions.
105+
#
106+
# @param [Array<Symbol>] The permissions to check for in their 2 letter abbreviated format, e.g. WP.
107+
# @param [String,Nil] An optional object ID that will be used for matching all the permissions.
108+
# @rtype MultipleAll
109+
def self.all(permissions, object_id: nil)
110+
permissions = Array.wrap(permissions)
111+
MultipleAll.new(permissions.map { |permission| new(permission, object_id: object_id) })
112+
end
113+
114+
def self.full_control
115+
# Full Control is special and shouldn't be split across multiple ACEs, so to check that we use #new instead of
116+
# MultipleAll to ensure it's in 1 ACE.
117+
new(%i[ CC DC LC SW RP WP DT LO CR SD RC WD WO ])
118+
end
119+
120+
def self.certificate_autoenrollment
121+
MultipleAny.new([
122+
Allow.new(:CR, object_id: CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT),
123+
full_control
124+
])
125+
end
126+
127+
# Build a matcher that will check for a certificate's enrollment permission.
128+
def self.certificate_enrollment
129+
MultipleAny.new([
130+
Allow.new(:CR, object_id: CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT),
131+
full_control
132+
])
133+
end
134+
end
135+
136+
# A compound matcher that will match when any of the sub-matchers match.
137+
class MultipleAny < Base
138+
attr_reader :matchers
139+
140+
def initialize(matchers)
141+
@matchers = Array.wrap(matchers)
142+
end
143+
144+
def ignore_ace?(ace)
145+
@matchers.all? { |matcher| matcher.ignore_ace?(ace) }
146+
end
147+
148+
def apply_ace!(ace)
149+
@matchers.each do |matcher|
150+
next if matcher.ignore_ace?(ace)
151+
152+
matcher.apply_ace!(ace)
153+
end
154+
155+
nil
156+
end
157+
158+
def satisfied?
159+
@matchers.any? { |matcher| matcher.satisfied? }
160+
end
161+
162+
def matches?
163+
@matchers.any? { |matcher| matcher.matches? }
164+
end
165+
end
166+
167+
# A compound matcher that will match when all of the sub-matchers match.
168+
class MultipleAll < MultipleAny
169+
def satisfied?
170+
@matchers.all? { |matcher| matcher.satisfied? }
171+
end
172+
173+
def matches?
174+
@matchers.all? { |matcher| matcher.matches? }
175+
end
176+
end
177+
end
178+
end
179+
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'lru_redux'
2+
3+
module Msf
4+
###
5+
#
6+
# This module exposes methods for querying a remote LDAP service
7+
#
8+
###
9+
module Exploit::Remote::LDAP
10+
module EntryCache
11+
class LDAPEntryCache < LruRedux::Cache
12+
def <<(entry)
13+
raise TypeError unless entry.is_a? Net::LDAP::Entry
14+
15+
self[entry.dn] = entry
16+
end
17+
18+
def get_by_dn(dn)
19+
self[dn]
20+
end
21+
22+
def get_by_samaccountname(samaccountname)
23+
entry = @data.values.reverse_each.find { _1[:sAMAccountName]&.first == samaccountname }
24+
@data[entry.dn] = entry if entry # update it as recently used
25+
entry
26+
end
27+
28+
def get_by_sid(sid)
29+
sid = Rex::Proto::MsDtyp::MsDtypSid.new(sid)
30+
31+
entry = @data.values.reverse_each.find { _1[:objectSid]&.first == sid.to_binary_s }
32+
@data[entry.dn] = entry if entry # update it as recently used
33+
entry
34+
end
35+
end
36+
37+
def ldap_entry_cache
38+
@ldap_entry_cache ||= LDAPEntryCache.new(1000)
39+
end
40+
end
41+
end
42+
end

0 commit comments

Comments
 (0)