CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/gather/enum_ad_users.rb
Views: 11655
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
'Compat' => {
44
'Meterpreter' => {
45
'Commands' => %w[
46
stdapi_net_resolve_host
47
]
48
}
49
}
50
)
51
)
52
53
register_options([
54
OptBool.new('STORE_LOOT', [true, 'Store file in loot.', false]),
55
OptBool.new('EXCLUDE_LOCKED', [true, 'Exclude in search locked accounts..', false]),
56
OptBool.new('EXCLUDE_DISABLED', [true, 'Exclude from search disabled accounts.', false]),
57
OptString.new('ADDITIONAL_FIELDS', [false, 'Additional fields to retrieve, comma separated', nil]),
58
OptString.new('FILTER', [false, 'Customised LDAP filter', nil]),
59
OptString.new('GROUP_MEMBER', [false, 'Recursively list users that are effectve members of the group DN specified.', nil]),
60
OptEnum.new('UAC', [
61
true, 'Filter on User Account Control Setting.', 'ANY',
62
[
63
'ANY',
64
'NO_PASSWORD',
65
'CHANGE_PASSWORD',
66
'NEVER_EXPIRES',
67
'SMARTCARD_REQUIRED',
68
'NEVER_LOGGEDON'
69
]
70
])
71
])
72
end
73
74
def run
75
@user_fields = USER_FIELDS.dup
76
77
if datastore['ADDITIONAL_FIELDS']
78
additional_fields = datastore['ADDITIONAL_FIELDS'].gsub(/\s+/, '').split(',')
79
@user_fields.push(*additional_fields)
80
end
81
82
max_search = datastore['MAX_SEARCH']
83
84
begin
85
q = query(query_filter, max_search, @user_fields)
86
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
87
# Can't bind or in a network w/ limited accounts
88
print_error(e.message)
89
return
90
end
91
92
if q.nil? || q[:results].empty?
93
print_status('No results returned.')
94
else
95
results_table = parse_results(q[:results])
96
print_line results_table.to_s
97
98
if datastore['STORE_LOOT']
99
stored_path = store_loot('ad.users', 'text/plain', session, results_table.to_csv)
100
print_good("Results saved to: #{stored_path}")
101
end
102
end
103
end
104
105
def account_disabled?(uac)
106
(uac & UAC_DISABLED) > 0
107
end
108
109
def account_locked?(lockout_time)
110
lockout_time > 0
111
end
112
113
# Takes the results of LDAP query, parses them into a table
114
# and records and usernames as {Metasploit::Credential::Core}s in
115
# the database.
116
#
117
# @param results [Array<Array<Hash>>] The LDAP query results to parse
118
# @return [Rex::Text::Table] the table containing all the result data
119
def parse_results(results)
120
domain = datastore['DOMAIN'] || get_domain
121
domain_ip = client.net.resolve.resolve_host(domain)[:ip]
122
# Results table holds raw string data
123
results_table = Rex::Text::Table.new(
124
'Header' => 'Domain Users',
125
'Indent' => 1,
126
'SortIndex' => -1,
127
'Columns' => @user_fields
128
)
129
130
results.each do |result|
131
row = []
132
133
result.each do |field|
134
if field.nil?
135
row << ''
136
else
137
row << field[:value]
138
end
139
end
140
141
username = result[@user_fields.index('sAMAccountName')][:value]
142
uac = result[@user_fields.index('userAccountControl')][:value]
143
lockout_time = result[@user_fields.index('lockoutTime')][:value]
144
store_username(username, uac, lockout_time, domain, domain_ip)
145
146
results_table << row
147
end
148
results_table
149
end
150
151
# Builds the LDAP query 'filter' used to find our User Accounts based on
152
# criteria set by user in the Datastore.
153
#
154
# @return [String] the LDAP query string
155
def query_filter
156
inner_filter = '(objectCategory=person)(objectClass=user)'
157
inner_filter << '(!(lockoutTime>=1))' if datastore['EXCLUDE_LOCKED']
158
inner_filter << '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' if datastore['EXCLUDE_DISABLED']
159
inner_filter << "(memberof:1.2.840.113556.1.4.1941:=#{datastore['GROUP_MEMBER']})" if datastore['GROUP_MEMBER']
160
inner_filter << "(#{datastore['FILTER']})" unless datastore['FILTER'].blank?
161
case datastore['UAC']
162
when 'ANY'
163
when 'NO_PASSWORD'
164
inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=32)'
165
when 'CHANGE_PASSWORD'
166
inner_filter << '(!sAMAccountType=805306370)(pwdlastset=0)'
167
when 'NEVER_EXPIRES'
168
inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=65536)'
169
when 'SMARTCARD_REQUIRED'
170
inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=262144)'
171
when 'NEVER_LOGGEDON'
172
inner_filter << '(|(lastlogon=0)(!lastlogon=*))'
173
end
174
"(&#{inner_filter})"
175
end
176
177
def store_username(username, uac, lockout_time, realm, domain_ip)
178
service_data = {
179
address: domain_ip,
180
port: 445,
181
service_name: 'smb',
182
protocol: 'tcp',
183
workspace_id: myworkspace_id
184
}
185
186
credential_data = {
187
origin_type: :session,
188
session_id: session_db_id,
189
post_reference_name: refname,
190
username: username,
191
realm_value: realm,
192
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
193
}
194
195
credential_data.merge!(service_data)
196
197
# Create the Metasploit::Credential::Core object
198
credential_core = create_credential(credential_data)
199
200
if account_disabled?(uac.to_i)
201
status = Metasploit::Model::Login::Status::DISABLED
202
elsif account_locked?(lockout_time.to_i)
203
status = Metasploit::Model::Login::Status::LOCKED_OUT
204
else
205
status = Metasploit::Model::Login::Status::UNTRIED
206
end
207
208
# Assemble the options hash for creating the Metasploit::Credential::Login object
209
login_data = {
210
core: credential_core,
211
status: status
212
}
213
214
login_data[:last_attempted_at] = DateTime.now unless (status == Metasploit::Model::Login::Status::UNTRIED)
215
216
# Merge in the service data and create our Login
217
login_data.merge!(service_data)
218
create_credential_login(login_data)
219
end
220
end
221
222