Skip to content

Commit e4324e6

Browse files
committed
Initial commit of AD DS LDAP mixin
1 parent 8ebb2dc commit e4324e6

File tree

2 files changed

+130
-1
lines changed

2 files changed

+130
-1
lines changed

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
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
module Msf
2+
###
3+
#
4+
# This module exposes methods for querying a remote LDAP service
5+
#
6+
###
7+
module Exploit::Remote::LDAP
8+
module ActiveDirectory
9+
include Msf::Exploit::Remote::LDAP
10+
11+
LDAP_CAP_ACTIVE_DIRECTORY_OID = '1.2.840.113556.1.4.800'.freeze
12+
13+
# Query the remote server via the provided LDAP connection to determine if it's an Active Directory LDAP server.
14+
# More specifically, this ensures that it reports active directory capabilities and the whoami extension.
15+
#
16+
# @param Net::LDAP::Connection ldap_connection
17+
# @rtype Boolean
18+
def is_active_directory?(ldap)
19+
root_dse = ldap.search(
20+
ignore_server_caps: true,
21+
base: '',
22+
scope: Net::LDAP::SearchScope_BaseObject,
23+
attributes: %i[ supportedCapabilities supportedExtension ]
24+
)&.first
25+
26+
return false unless root_dse[:supportedCapabilities].map(&:to_s).include?(LDAP_CAP_ACTIVE_DIRECTORY_OID)
27+
28+
return false unless root_dse[:supportedExtension].include?(Net::LDAP::WhoamiOid)
29+
30+
true
31+
end
32+
33+
def adds_query_group_members(ldap, group_dn, base_dn: nil, inherited: true, object_class: nil)
34+
return enum_for(:adds_query_group_members, ldap, group_dn, base_dn: base_dn, inherited: inherited, object_class: object_class) unless block_given?
35+
results = 0
36+
37+
member_filter = "memberOf#{inherited ? ':1.2.840.113556.1.4.1941:' : ''}=#{ldap_escape_filter(group_dn)}"
38+
39+
# Get the member's primaryGroupID
40+
group = adds_get_object_by_dn(ldap, group_dn)
41+
if group && group[:objectSID]
42+
group_sid = Rex::Proto::MsDtyp::MsDtypSid.read(group[:objectSID].first)
43+
# if we have a group RID, filter on that when the object has it as it's primaryGroupId to include those goups too
44+
member_filter = "|(#{member_filter})(primaryGroupId=#{group_sid.rid})"
45+
end
46+
47+
filters = []
48+
filters << "objectClass=#{ldap_escape_filter(object_class)}" if object_class
49+
filters << member_filter
50+
51+
ldap.search(
52+
base: base_dn || ldap.base_dn,
53+
filter: "(&#{filters.map { "(#{_1})" }.join})",
54+
return_result: false # make sure we're streaming because this could be a lot of data
55+
) do |ldap_entry|
56+
yield ldap_entry
57+
results += 1
58+
end
59+
60+
unless ldap.get_operation_result.code == 0
61+
raise "LDAP Error: #{@ldap.get_operation_result.message}"
62+
end
63+
64+
results
65+
end
66+
67+
def adds_query_member_groups(ldap, member_dn, base_dn: nil, inherited: true)
68+
return enum_for(:adds_query_member_groups, ldap, member_dn, base_dn: base_dn, inherited: inherited) unless block_given?
69+
results = 0
70+
71+
# Get the member's primaryGroupId
72+
member = adds_get_object_by_dn(ldap, member_dn)
73+
if member && member[:objectSid] && member[:primaryGroupId] && !member[:primaryGroupId].empty?
74+
# if it's found, calculate the SID of the primary group and query it, the primary group is typically 'Domain Users'
75+
# and is *not* included in the member query
76+
member_sid = Rex::Proto::MsDtyp::MsDtypSid.read(member[:objectSid].first)
77+
primary_group_sid = "#{member_sid.to_s.rpartition('-').first}-#{member[:primaryGroupId].first}"
78+
primary_group = adds_get_object_by_sid(ldap, primary_group_sid)
79+
yield primary_group if primary_group
80+
end
81+
82+
filters = []
83+
filters << "objectClass=group"
84+
filters << "member#{inherited ? ':1.2.840.113556.1.4.1941:' : ''}=#{ldap_escape_filter(member_dn)}"
85+
86+
ldap.search(
87+
base: base_dn || ldap.base_dn,
88+
filter: "(&#{filters.map { "(#{_1})" }.join})",
89+
return_result: false
90+
) do |ldap_entry|
91+
yield ldap_entry
92+
results += 1
93+
end
94+
95+
unless ldap.get_operation_result.code == 0
96+
raise "LDAP Error: #{ldap.get_operation_result.message}"
97+
end
98+
99+
results
100+
end
101+
102+
def adds_get_object_by_dn(ldap, object_dn)
103+
@ldap_objects ||= []
104+
object = @ldap_objects.find { |o| o[:dN]&.first == object_dn }
105+
return object if object
106+
107+
object = ldap.search(base: object_dn, scope: Net::LDAP::SearchScope_BaseObject)&.first
108+
validate_query_result!(ldap.get_operation_result.table)
109+
110+
@ldap_objects << object if object
111+
object
112+
end
113+
114+
def adds_get_object_by_sid(ldap, object_sid)
115+
@ldap_objects ||= []
116+
object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid)
117+
object = @ldap_objects.find { |o| o[:objectSid]&.first == object_sid.to_binary_s }
118+
return object if object
119+
120+
filter = "(objectSID=#{ldap_escape_filter(object_sid.to_s)})"
121+
object = ldap.search(base: ldap.base_dn, filter: filter)&.first
122+
validate_query_result!(ldap.get_operation_result.table, filter)
123+
124+
@ldap_objects << object if object
125+
object
126+
end
127+
end
128+
end
129+
end

0 commit comments

Comments
 (0)