Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/gather/enum_ad_users.rb
19778 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
include Msf::Auxiliary::Report
8
include Msf::Post::Windows::LDAP
9
include Msf::Post::Windows::Accounts
10
11
UAC_DISABLED = 0x02
12
USER_FIELDS = [
13
'sAMAccountName',
14
'name',
15
'userPrincipalName',
16
'userAccountControl',
17
'lockoutTime',
18
'mail',
19
'primarygroupid',
20
'description'
21
].freeze
22
23
def initialize(info = {})
24
super(
25
update_info(
26
info,
27
'Name' => 'Windows Gather Active Directory Users',
28
'Description' => %q{
29
This module will enumerate user accounts in the default Active Domain (AD) directory and stores
30
them in the database. If GROUP_MEMBER is set to the DN of a group, this will list the members of
31
that group by performing a recursive/nested search (i.e. it will list users who are members of
32
groups that are members of groups that are members of groups (etc) which eventually include the
33
target group DN.
34
},
35
'License' => MSF_LICENSE,
36
'Author' => [
37
'Ben Campbell',
38
'Carlos Perez <carlos_perez[at]darkoperator.com>',
39
'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>'
40
],
41
'Platform' => [ 'win' ],
42
'SessionTypes' => [ 'meterpreter' ],
43
'Notes' => {
44
'Stability' => [CRASH_SAFE],
45
'SideEffects' => [],
46
'Reliability' => []
47
},
48
'Compat' => {
49
'Meterpreter' => {
50
'Commands' => %w[
51
stdapi_net_resolve_host
52
]
53
}
54
}
55
)
56
)
57
58
register_options([
59
OptBool.new('STORE_LOOT', [true, 'Store file in loot.', false]),
60
OptBool.new('EXCLUDE_LOCKED', [true, 'Exclude in search locked accounts..', false]),
61
OptBool.new('EXCLUDE_DISABLED', [true, 'Exclude from search disabled accounts.', false]),
62
OptString.new('ADDITIONAL_FIELDS', [false, 'Additional fields to retrieve, comma separated', nil]),
63
OptString.new('FILTER', [false, 'Customised LDAP filter', nil]),
64
OptString.new('GROUP_MEMBER', [false, 'Recursively list users that are effectve members of the group DN specified.', nil]),
65
OptEnum.new('UAC', [
66
true, 'Filter on User Account Control Setting.', 'ANY',
67
[
68
'ANY',
69
'NO_PASSWORD',
70
'CHANGE_PASSWORD',
71
'NEVER_EXPIRES',
72
'SMARTCARD_REQUIRED',
73
'NEVER_LOGGEDON'
74
]
75
])
76
])
77
end
78
79
def run
80
@user_fields = USER_FIELDS.dup
81
82
if datastore['ADDITIONAL_FIELDS']
83
additional_fields = datastore['ADDITIONAL_FIELDS'].gsub(/\s+/, '').split(',')
84
@user_fields.push(*additional_fields)
85
end
86
87
max_search = datastore['MAX_SEARCH']
88
89
begin
90
q = query(query_filter, max_search, @user_fields)
91
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
92
# Can't bind or in a network w/ limited accounts
93
print_error(e.message)
94
return
95
end
96
97
if q.nil? || q[:results].empty?
98
print_status('No results returned.')
99
else
100
results_table = parse_results(q[:results])
101
print_line results_table.to_s
102
103
if datastore['STORE_LOOT']
104
stored_path = store_loot('ad.users', 'text/plain', session, results_table.to_csv)
105
print_good("Results saved to: #{stored_path}")
106
end
107
end
108
end
109
110
def account_disabled?(uac)
111
(uac & UAC_DISABLED) > 0
112
end
113
114
def account_locked?(lockout_time)
115
lockout_time > 0
116
end
117
118
# Takes the results of LDAP query, parses them into a table
119
# and records and usernames as {Metasploit::Credential::Core}s in
120
# the database.
121
#
122
# @param results [Array<Array<Hash>>] The LDAP query results to parse
123
# @return [Rex::Text::Table] the table containing all the result data
124
def parse_results(results)
125
domain = datastore['DOMAIN'] || get_domain
126
domain_ip = client.net.resolve.resolve_host(domain)[:ip]
127
# Results table holds raw string data
128
results_table = Rex::Text::Table.new(
129
'Header' => 'Domain Users',
130
'Indent' => 1,
131
'SortIndex' => -1,
132
'Columns' => @user_fields
133
)
134
135
results.each do |result|
136
row = []
137
138
result.each do |field|
139
if field.nil?
140
row << ''
141
else
142
row << field[:value]
143
end
144
end
145
146
username = result[@user_fields.index('sAMAccountName')][:value]
147
uac = result[@user_fields.index('userAccountControl')][:value]
148
lockout_time = result[@user_fields.index('lockoutTime')][:value]
149
store_username(username, uac, lockout_time, domain, domain_ip)
150
151
results_table << row
152
end
153
results_table
154
end
155
156
# Builds the LDAP query 'filter' used to find our User Accounts based on
157
# criteria set by user in the Datastore.
158
#
159
# @return [String] the LDAP query string
160
def query_filter
161
inner_filter = '(objectCategory=person)(objectClass=user)'
162
inner_filter << '(!(lockoutTime>=1))' if datastore['EXCLUDE_LOCKED']
163
inner_filter << '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' if datastore['EXCLUDE_DISABLED']
164
inner_filter << "(memberof:1.2.840.113556.1.4.1941:=#{datastore['GROUP_MEMBER']})" if datastore['GROUP_MEMBER']
165
inner_filter << "(#{datastore['FILTER']})" unless datastore['FILTER'].blank?
166
case datastore['UAC']
167
when 'ANY'
168
# no filter
169
when 'NO_PASSWORD'
170
inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=32)'
171
when 'CHANGE_PASSWORD'
172
inner_filter << '(!sAMAccountType=805306370)(pwdlastset=0)'
173
when 'NEVER_EXPIRES'
174
inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=65536)'
175
when 'SMARTCARD_REQUIRED'
176
inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=262144)'
177
when 'NEVER_LOGGEDON'
178
inner_filter << '(|(lastlogon=0)(!lastlogon=*))'
179
end
180
"(&#{inner_filter})"
181
end
182
183
def store_username(username, uac, lockout_time, realm, domain_ip)
184
service_data = {
185
address: domain_ip,
186
port: 445,
187
service_name: 'smb',
188
protocol: 'tcp',
189
workspace_id: myworkspace_id
190
}
191
192
credential_data = {
193
origin_type: :session,
194
session_id: session_db_id,
195
post_reference_name: refname,
196
username: username,
197
realm_value: realm,
198
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
199
}
200
201
credential_data.merge!(service_data)
202
203
# Create the Metasploit::Credential::Core object
204
credential_core = create_credential(credential_data)
205
206
if account_disabled?(uac.to_i)
207
status = Metasploit::Model::Login::Status::DISABLED
208
elsif account_locked?(lockout_time.to_i)
209
status = Metasploit::Model::Login::Status::LOCKED_OUT
210
else
211
status = Metasploit::Model::Login::Status::UNTRIED
212
end
213
214
# Assemble the options hash for creating the Metasploit::Credential::Login object
215
login_data = {
216
core: credential_core,
217
status: status
218
}
219
220
login_data[:last_attempted_at] = DateTime.now unless (status == Metasploit::Model::Login::Status::UNTRIED)
221
222
# Merge in the service data and create our Login
223
login_data.merge!(service_data)
224
create_credential_login(login_data)
225
end
226
end
227
228