CVE: CVE-2021-0254

Tested Versions:

  • Junos OS 15.1 to 20.4R1 (Tested on Juniper MX960 device)

Product URL(s):

Description of the vulnerability

overlayd is a service that handles Overlay OAM Packet send to Juniper device. This service runs as root by default when the device starts and listens to the UDP connection on port 4789. Port 4789 is exposed to the internet, and everyone can connect to this port and send data. The specific flaw exists within the parsing packet function in the overlayd service. The issue results from the lack of proper validation of the size of the buffer before copying this data to a bss buffer, which can lead to bss overflow. Unauthenticated attackers can send specially crafted packets to trigger this vulnerability and resulting in remote code execution on the device.

overlayd is a service that handles Overlay OAM Packet send to Juniper device (https://www.juniper.net/documentation/en_US/junos/topics/concept/overlay-ping-traceroute-packet-understanding.html). To easily debug this, we could enable debugging log of overlayd service by running several CLI commands in Junos OS system to allow traceoptions to of this service.

set protocols overlay traceoptions file overlay_tracer files 3 size 50m
set protocols overlay traceoptions level all
set protocols overlay traceoptions flag all

The debug log file is /var/log/overlay_tracer.

The function sub_8051270 handles the overlay OAM packet:

int __cdecl sub_8051270(int a1)
{
  for ( result = task_recv_pkt(a1, (int)&v5); !result; result = task_recv_pkt(a1, (int)&v5) ) // <- recveive packet
  {
    off_845BFA8->sin_addr.s_addr = *(_DWORD *)(get_src_sockaddr_from_ctx() + 4);
    off_845BFA8->sin_port = *(_WORD *)(get_src_sockaddr_from_ctx() + 2);
    off_845BFAC->sin_addr.s_addr = *(_DWORD *)(get_dst_sockaddr_from_ctx() + 4);
    off_845BFAC->sin_port = *(_WORD *)(get_dst_sockaddr_from_ctx() + 2);
    src = (void *)get_recv_buff_from_ctx();
    buff_size = v5;
    fd = sock_fd;
    v3 = sub_806AD20();
    v4 = sub_806ACC0();
    overlayd_parse_oam_packet(src, buff_size, fd, &src_addr, &dst_addr, v3, v4); // <- parse packet
  }
  return result;
}

(The above pseudo-code snippet is generated by IDA hexray. Some functions and variables were renamed for easy reading) The first function, task_recv_pkt (renamed form sub_80917B0), is called to receive the packet.

int __cdecl task_recv_pkt(int a1, int a2)
{
  struc_5 **v2; // eax

  if ( !a1 || (v2 = *(void ***)(a1 + 140)) == 0 )
    v2 = (struc_5 **)sub_8094B80();
  return task_receive_packet_internal(a1, (ssize_t *)a2, v2[3]->recv_buff, v2[3]->recv_size, 1);
}

signed int __cdecl task_receive_packet_internal(int a1, ssize_t *a2, int recv_buff, int recv_size, char a5)
{
  v5 = a1;
  message.msg_name = 0;
  message.msg_namelen = 0;
  message.msg_iov = &v63;
  message.msg_iovlen = 1;
  message.msg_control = 0;
  message.msg_controllen = 0;
  message.msg_flags = 0;
  if ( !a1 )
  {
    sub_8094B80();
    *a2 = 0;
    return 22;
  }
  v6 = a5;
  v7 = *(void ***)(a1 + 140);
  if ( !v7 )
  {
    v7 = sub_8094B80();
    v6 = a5;
  }
  v8 = (int)v7[3];
  v60 = v8;
  if ( v6 )
  {
    v9 = *(unsigned __int8 **)(v8 + 60);
    if ( v9 )
    {
      sub_80A46D0(v9);
      *(_DWORD *)(v60 + 60) = 0;
    }
  }
  v63.iov_base = (void *)recv_buff;
  v63.iov_len = recv_size;
  message.msg_name = &v72;
  message.msg_namelen = 128;
  message.msg_control = &v73;
  message.msg_controllen = 896;
  v10 = recvmsg(*(_DWORD *)(a1 + 156), &message, 0); // <- receive packet

  ...

Function task_receive_packet_internal (renamed from sub_808FD60) call recvmsg function to receive data from udp socket port 4789 up to 0x10000 bytes.

The next, function overlayd_parse_oam_packet (renamed from sub_8052280) is called to parse the received packet.

int __cdecl overlayd_parse_oam_packet(void *buff, size_t buffsize, int fd, struct sockaddr_in *saddr, struct sockaddr_in *daddr, __int16 a6, int a7)
{

  if ( (signed int)buffsize < 90 || !(_byteswap_ulong(*(_DWORD *)buff) & 0x1000000) )      // <- check size and the first byte of received packet
  {
    v7 = getpid();
    return daemon_trace(&unk_8C64198, -1, 256, "C(%d-%s): packet parsing failed\n", v7, "overlayd_recv_oam_packet");
  }
  src_ip = _byteswap_ulong(saddr->sin_addr.s_addr);
  src_port = (unsigned __int16)__ROL2__(saddr->sin_port, 8);
  dst_ip = _byteswap_ulong(daddr->sin_addr.s_addr);
  v56 = getpid();
  v11 = src_ip >> 24;
  v55 = BYTE2(src_ip);
  v54 = BYTE1(src_ip);
  v12 = (unsigned __int8)src_ip;
  v13 = saddr;
  daemon_trace(
    &unk_8C64198,
    -1,
    256,
    "C(%d-%s): Received UDP packet from %u.%u.%u.%u port %u to %u.%u.%u.%u port %u\n",
    v56,
    "overlayd_parse_oam_packet",
    v11,
    v55,
    v54,
    v12,
    src_port,
    dst_ip >> 24,
    BYTE2(dst_ip),
    BYTE1(dst_ip),
    (unsigned __int8)dst_ip,
    4789);
  memset(byte_8C655FC, 0, 0x66u);
  dword_8C655E0 = 0x82000045;
  dword_8C655E4 = 0;
  dword_8C655E8 = 4607;
  dword_8C655EC = daddr->sin_addr.s_addr;
  dword_8C655F0 = saddr->sin_addr.s_addr;
  dword_8C655F4 = 0xB512B512;
  dword_8C655F8 = 28160;
  memcpy(byte_8C655FC, buff, buffsize);        // <- bss overflow
  ...

To reach the vulnerable code, the packet must pass the check at the first line of function overlayd_parse_oam_packet. The packet size must be greater than 90 bytes, and the first byte of packet must be even. When the memcpy function is called, due to the lack of proper validation of packet size before copying to bss buffer at address 0x8C655FC, it results in a bss buffer overflow vulnerability.

Debugging

There is mitigation, veriexec, which protects the Junos operating system (OS) against unauthorized software, so we can’t debug the program directly in Junos os by using gdb. Instead, we may generate the core dump file of overlayd process with two methods. The first method is using the command gcore <overlayd-pid>. The second one is sending a long packet to crash the overlayd process, and the core dump file of the crashed process is automatically generated at /var/tmp/overlayd.core-tarball.X.tgz. After that, the core dump file can be loaded to gdb. Because Junos OS is based on FreeBSD and overlayd binary is 32bit, you could use gdb on FreeBSD 32bit.

The Exploitation

With the ability to overwrite data after address 0x8C655FC up to 0x10000 bytes, we can overwrite the data at address 0x08C69C58 (dword_8C69C58) which is an array of function pointer. dword_8C69C58 is used in many functions, one of them is called right before receiving packet, in function sub_80A46D0 (sub_8051270 -> sub_80917B0 (task_recv_pkt) -> sub_808FD60 (task_receive_packet_internal) -> sub_80A46D0):

void __cdecl sub_80A46D0(unsigned __int8 *a1)
{

  if ( a1 )
  {
    v1 = sub_80A5B00(a1[1], a1[1]);
    if ( v1 )
    {
      v2 = *(void (__cdecl **)(unsigned __int8 *))(v1 + 236);
      if ( v2 )
        v2(a1);
    }
    v3 = *(void (__cdecl **)(unsigned __int8 *, _DWORD, int))(dword_8C69C58 + 0x40); <- usage of dword_8C69C58
    if ( v3 )
    {
      switch ( a1[1] )
      {
        case 0xEu:
        case 0xFu:
        case 0x20u:
        case 0x21u:
          v4 = *((unsigned __int16 *)a1 + 1);
          break;
        default:
          v4 = *a1;
          break;
      }
      v3(a1, a1[1], v4);
	}
	...
  }
}

(IDA Hexray generates the above pseudo-code snippet) The exploitation approach is crafting a fake function pointer array in bss, and then overwrite the value of dword_8C69C58 with the address of the fake array. Then we can control the value of the EIP register when the function reaches the call v3(a1, a1[1], v4);.

But there is one thing we must do first. The below pseudo-code snippet is in vulnerability function overlayd_parse_oam_packet:

int __cdecl overlayd_parse_oam_packet(void *buff, size_t buffsize, int fd, struct sockaddr_in *saddr, struct sockaddr_in *daddr, __int16 a6, int a7)
{
  if ( byte_8C6562E == 1 )
  {
		// Do some complicated things 
  }
  return result;
}

If the value of byte_8C6562E (which is at offset 0x32 from address 0x8C655FC - overflowed bss buffer) equals 1. A complicated part of the code is executed, and it may lead to some unexpected crashes before reaching the desired point at function sub_80A46D0. Therefore, when crafting the packet, we must set data at offset 0x32 of the packet to a value of 0.

To successfully execute commands in the Junos OS, we use the ROP chain technique. The snippet below is a sample ROP chain that I used in my exploit:

.text:080A469C                 mov     eax, ds:dword_8C69C58
.text:080A46A1                 mov     esi, ecx
.text:080A46A3                 push    ecx
.text:080A46A4                 push    ebx
.text:080A46A5                 call    dword ptr [eax]

The first gadget is used for moving the value of dword_8C69C58 to the EAX register, and call the function store at [EAX]. Recall that we already overwrite the value of dword_8C69C58 with the address of the fake function pointer array.

.text:08378A03                 push    eax
.text:08378A04                 call    dword ptr [eax+28h]

The second one is used to push the EAX register to stack for first argument, then call the function at [EAX+0x28]. If we put the system function address at [EAX+0x28], we can call this function with the controllable command and achieve remote command execution in the target system. In summary, the fake function pointer array is looked like bellow:

0x0         0x4                 0x28                0x40
+------------+-------------+-----+-------------+-----+------------+
|            |             |     |             |     |            |
| 0x08378A03 | ;<command>; | ... | system_addr | ... | 0x080A469C |
|            |             |     |             |     |            |
+------------+-------------+-----+-------------+-----+------------+

Timeline

  • 2020-10-08 Reported to Vendor
  • 2020-10-16 Vendor acknowledged the vulnerability report
  • 2021-04-14 Vendor patched and disclosed the vulnerability