Land #5102, @wchen-r7's ManageEngine Desktop Central Login Utility
commit
c6806b4e5f
|
@ -0,0 +1,135 @@
|
|||
|
||||
require 'metasploit/framework/login_scanner/http'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
class ManageEngineDesktopCentral < HTTP
|
||||
|
||||
DEFAULT_PORT = 8020
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
LOGIN_STATUS = Metasploit::Model::Login::Status # Shorter name
|
||||
|
||||
|
||||
# Checks if the target is ManageEngine Dekstop Central.
|
||||
#
|
||||
# @return [Boolean] TrueClass if target is MSP, otherwise FalseClass
|
||||
def check_setup
|
||||
login_uri = normalize_uri("#{uri}/configurations.do")
|
||||
res = send_request({'uri' => login_uri})
|
||||
|
||||
if res && res.body.include?('ManageEngine Desktop Central')
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
|
||||
# Returns the latest sid from MSP
|
||||
#
|
||||
# @param [Rex::Proto::Http::Response]
|
||||
# @return [String] The session ID for MSP
|
||||
def get_sid(res)
|
||||
cookies = res.get_cookies
|
||||
sid = cookies.scan(/(DCJSESSIONID=\w+);*/).flatten[0] || ''
|
||||
sid
|
||||
end
|
||||
|
||||
|
||||
|
||||
# Returns the hidden inputs
|
||||
#
|
||||
# @param [Rex::Proto::Http::Response]
|
||||
# @return [Hash] Input fields
|
||||
def get_hidden_inputs(res)
|
||||
found_inputs = {}
|
||||
res.body.scan(/(<input type="hidden" .+>)/).flatten.each do |input|
|
||||
name = input.scan(/name="(\w+)"/).flatten[0] || ''
|
||||
value = input.scan(/value="([\w\.\-]+)"/).flatten[0] || ''
|
||||
found_inputs[name] = value
|
||||
end
|
||||
found_inputs
|
||||
end
|
||||
|
||||
|
||||
# Returns all the items needed to login
|
||||
#
|
||||
# @return [Hash] Login items
|
||||
def get_required_login_items
|
||||
items = {}
|
||||
login_uri = normalize_uri("#{uri}/configurations.do")
|
||||
res = send_request({'uri' => login_uri})
|
||||
return items unless res
|
||||
items.merge!({'sid' => get_sid(res)})
|
||||
items.merge!(get_hidden_inputs(res))
|
||||
items
|
||||
end
|
||||
|
||||
|
||||
# Actually doing the login. Called by #attempt_login
|
||||
#
|
||||
# @param username [String] The username to try
|
||||
# @param password [String] The password to try
|
||||
# @return [Hash]
|
||||
# * :status [Metasploit::Model::Login::Status]
|
||||
# * :proof [String] the HTTP response body
|
||||
def get_login_state(username, password)
|
||||
login_uri = normalize_uri("#{uri}/j_security_check")
|
||||
login_items = get_required_login_items
|
||||
|
||||
res = send_request({
|
||||
'uri' => login_uri,
|
||||
'method' => 'POST',
|
||||
'cookie' => login_items['sid'],
|
||||
'vars_post' => {
|
||||
'j_username' => username,
|
||||
'j_password' => password,
|
||||
'Button' => 'Sign+in',
|
||||
'buildNum' => login_items['buildNum'],
|
||||
'clearCacheBuildNum' => login_items['clearCacheBuildNum']
|
||||
}
|
||||
})
|
||||
|
||||
unless res
|
||||
return {:status => LOGIN_STATUS::UNABLE_TO_CONNECT, :proof => res.to_s}
|
||||
end
|
||||
|
||||
if res.code == 302
|
||||
return {:status => LOGIN_STATUS::SUCCESSFUL, :proof => res.to_s}
|
||||
end
|
||||
|
||||
{:status => LOGIN_STATUS::INCORRECT, :proof => res.to_s}
|
||||
end
|
||||
|
||||
|
||||
# Attempts to login to MSP.
|
||||
#
|
||||
# @param credential [Metasploit::Framework::Credential] The credential object
|
||||
# @return [Result] A Result object indicating success or failure
|
||||
def attempt_login(credential)
|
||||
result_opts = {
|
||||
credential: credential,
|
||||
status: LOGIN_STATUS::INCORRECT,
|
||||
proof: nil,
|
||||
host: host,
|
||||
port: port,
|
||||
protocol: 'tcp'
|
||||
}
|
||||
|
||||
begin
|
||||
result_opts.merge!(get_login_state(credential.public, credential.private))
|
||||
rescue ::Rex::ConnectionError => e
|
||||
# Something went wrong during login. 'e' knows what's up.
|
||||
result_opts.merge!(status: LOGIN_STATUS::UNABLE_TO_CONNECT, proof: e.message)
|
||||
end
|
||||
|
||||
Result.new(result_opts)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'metasploit/framework/login_scanner/manageengine_desktop_central'
|
||||
require 'metasploit/framework/credential_collection'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Auxiliary::AuthBrute
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Auxiliary::Scanner
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => 'ManageEngine Desktop Central Login Utility',
|
||||
'Description' => %q{
|
||||
This module will attempt to authenticate to a ManageEngine Desktop Central.
|
||||
},
|
||||
'Author' => [ 'sinn3r' ],
|
||||
'License' => MSF_LICENSE,
|
||||
'DefaultOptions' => { 'RPORT' => 8020}
|
||||
))
|
||||
end
|
||||
|
||||
|
||||
# Initializes CredentialCollection and ManageEngineDesktopCentral
|
||||
def init(ip)
|
||||
@cred_collection = Metasploit::Framework::CredentialCollection.new(
|
||||
blank_passwords: datastore['BLANK_PASSWORDS'],
|
||||
pass_file: datastore['PASS_FILE'],
|
||||
password: datastore['PASSWORD'],
|
||||
user_file: datastore['USER_FILE'],
|
||||
userpass_file: datastore['USERPASS_FILE'],
|
||||
username: datastore['USERNAME'],
|
||||
user_as_pass: datastore['USER_AS_PASS']
|
||||
)
|
||||
|
||||
@scanner = Metasploit::Framework::LoginScanner::ManageEngineDesktopCentral.new(
|
||||
configure_http_login_scanner(
|
||||
host: ip,
|
||||
port: datastore['RPORT'],
|
||||
cred_details: @cred_collection,
|
||||
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
|
||||
connection_timeout: 5
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
# Reports a good login credential
|
||||
def do_report(ip, port, result)
|
||||
service_data = {
|
||||
address: ip,
|
||||
port: port,
|
||||
service_name: 'http',
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
credential_data = {
|
||||
module_fullname: self.fullname,
|
||||
origin_type: :service,
|
||||
private_data: result.credential.private,
|
||||
private_type: :password,
|
||||
username: result.credential.public,
|
||||
}.merge(service_data)
|
||||
|
||||
login_data = {
|
||||
core: create_credential(credential_data),
|
||||
last_attempted_at: DateTime.now,
|
||||
status: result.status,
|
||||
proof: result.proof
|
||||
}.merge(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
end
|
||||
|
||||
|
||||
# Attempts to login
|
||||
def bruteforce(ip)
|
||||
@scanner.scan! do |result|
|
||||
case result.status
|
||||
when Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
print_brute(:level => :good, :ip => ip, :msg => "Success: '#{result.credential}'")
|
||||
do_report(ip, rport, result)
|
||||
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
vprint_brute(:level => :verror, :ip => ip, :msg => result.proof)
|
||||
invalidate_login(
|
||||
address: ip,
|
||||
port: rport,
|
||||
protocol: 'tcp',
|
||||
public: result.credential.public,
|
||||
private: result.credential.private,
|
||||
realm_key: result.credential.realm_key,
|
||||
realm_value: result.credential.realm,
|
||||
status: result.status,
|
||||
proof: result.proof
|
||||
)
|
||||
when Metasploit::Model::Login::Status::INCORRECT
|
||||
vprint_brute(:level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'")
|
||||
invalidate_login(
|
||||
address: ip,
|
||||
port: rport,
|
||||
protocol: 'tcp',
|
||||
public: result.credential.public,
|
||||
private: result.credential.private,
|
||||
realm_key: result.credential.realm_key,
|
||||
realm_value: result.credential.realm,
|
||||
status: result.status,
|
||||
proof: result.proof
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Start here
|
||||
def run_host(ip)
|
||||
init(ip)
|
||||
unless @scanner.check_setup
|
||||
print_brute(:level => :error, :ip => ip, :msg => 'Target is not ManageEngine Desktop Central')
|
||||
return
|
||||
end
|
||||
|
||||
bruteforce(ip)
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,156 @@
|
|||
require 'spec_helper'
|
||||
require 'metasploit/framework/login_scanner/manageengine_desktop_central'
|
||||
|
||||
describe Metasploit::Framework::LoginScanner::ManageEngineDesktopCentral do
|
||||
|
||||
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
|
||||
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
|
||||
|
||||
let(:session_id) do
|
||||
'DCJSESSIONID=5628CFEA339C2688D74267B03CDA88BD; '
|
||||
end
|
||||
|
||||
let(:username) do
|
||||
'username'
|
||||
end
|
||||
|
||||
let(:good_password) do
|
||||
'good_password'
|
||||
end
|
||||
|
||||
let(:bad_password) do
|
||||
'bad_password'
|
||||
end
|
||||
|
||||
let(:successful_auth_response) do
|
||||
Rex::Proto::Http::Response.new(302, 'Moved Temporarily')
|
||||
end
|
||||
|
||||
let(:fail_auth_response) do
|
||||
Rex::Proto::Http::Response.new(200, 'OK')
|
||||
end
|
||||
|
||||
subject do
|
||||
described_class.new
|
||||
end
|
||||
|
||||
let(:response) do
|
||||
Rex::Proto::Http::Response.new(200, 'OK')
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:request_cgi).with(any_args)
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv).with(any_args).and_return(response)
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:set_config).with(any_args)
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:close)
|
||||
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect)
|
||||
end
|
||||
|
||||
describe '#check_setup' do
|
||||
context 'when target is ManageEngine Desktop Central' do
|
||||
let(:response) do
|
||||
res = Rex::Proto::Http::Response.new(200, 'OK')
|
||||
res.body = 'ManageEngine Desktop Central'
|
||||
res
|
||||
end
|
||||
it 'returns true' do
|
||||
expect(subject.check_setup).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when target is not ManageEngine Desktop Central' do
|
||||
it 'returns false' do
|
||||
expect(subject.check_setup).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_sid' do
|
||||
context 'when there is no session ID' do
|
||||
let(:response) do
|
||||
res = Rex::Proto::Http::Response.new(200, 'OK')
|
||||
res.headers['Set-Cookie'] = session_id
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
it 'returns a new session ID' do
|
||||
expect(subject.get_sid(response)).to include('DCJSESSIONID')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_hidden_inputs' do
|
||||
let(:response) do
|
||||
html = %Q|
|
||||
<input type="hidden" name="buildNum" id="buildNum" value="90109"/>
|
||||
<input type="hidden" name="clearCacheBuildNum" id="clearCacheBuildNum" value="-1"/>
|
||||
|
|
||||
res = Rex::Proto::Http::Response.new(200, 'OK')
|
||||
res.body = html
|
||||
res
|
||||
end
|
||||
|
||||
context 'when there are hidden login inputs' do
|
||||
it 'returns a Hash' do
|
||||
expect(subject.get_hidden_inputs(response)).to be_kind_of(Hash)
|
||||
end
|
||||
|
||||
it 'returns the value for buildNum' do
|
||||
expect(subject.get_hidden_inputs(response)['buildNum']).to eq('90109')
|
||||
end
|
||||
|
||||
it 'returns the value for clearCacheBuildNum' do
|
||||
expect(subject.get_hidden_inputs(response)['clearCacheBuildNum']).to eq('-1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_login_state' do
|
||||
context 'when the credential is valid' do
|
||||
let(:response) { successful_auth_response }
|
||||
it 'returns a hash indicating a successful login' do
|
||||
expect(subject.get_login_state(username, good_password)[:status]).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the creential is invalid' do
|
||||
let(:response) { fail_auth_response }
|
||||
it 'returns a hash indicating an incorrect cred' do
|
||||
expect(subject.get_login_state(username, good_password)[:status]).to eq(Metasploit::Model::Login::Status::INCORRECT)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#attempt_login' do
|
||||
context 'when the credential is valid' do
|
||||
let(:response) { successful_auth_response }
|
||||
let(:cred_obj) { Metasploit::Framework::Credential.new(public: username, private: good_password) }
|
||||
|
||||
it 'returns a Result object indicating a successful login' do
|
||||
result = subject.attempt_login(cred_obj)
|
||||
expect(result).to be_kind_of(::Metasploit::Framework::LoginScanner::Result)
|
||||
end
|
||||
|
||||
it 'returns successful login' do
|
||||
result = subject.attempt_login(cred_obj)
|
||||
expect(result.status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the credential is invalid' do
|
||||
let(:response) { fail_auth_response }
|
||||
let(:cred_obj) { Metasploit::Framework::Credential.new(public: username, private: bad_password) }
|
||||
|
||||
it 'returns a Result object' do
|
||||
result = subject.attempt_login(cred_obj)
|
||||
expect(result).to be_kind_of(::Metasploit::Framework::LoginScanner::Result)
|
||||
end
|
||||
|
||||
it 'returns incorrect credential status' do
|
||||
result = subject.attempt_login(cred_obj)
|
||||
expect(result.status).to eq(Metasploit::Model::Login::Status::INCORRECT)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue