Talos Vulnerability Report

TALOS-2022-1450

Reolink RLC-410W netserver recv_command denial of service vulnerability

January 26, 2022
CVE Number

CVE-2022-21801

Summary

A denial of service vulnerability exists in the netserver recv_command functionality of reolink RLC-410W v3.0.0.136_20121102. A specially-crafted network request can lead to a reboot. An attacker can send a malicious packet to trigger this vulnerability.

Tested Versions

Reolink RLC-410W v3.0.0.136_20121102

Product URLs

RLC-410W - https://reolink.com/us/product/rlc-410w/

CVSSv3 Score

8.6 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H

CWE

CWE-190 - Integer Overflow or Wraparound

Details

The reolink RLC-410W is a WiFi security camera. The camera includes motion detection functionalities and various methods to save the recordings.

A specially crafted request can lead to an integer overflow in the calculation of a buffer size. This can result in not allocating a buffer that will be dereference nevertheless. This would lead to a denial of service vulnerability due to the null pointer dereference.

The RLC-410W offers several APIs functionalities, once logged, through a binary called netserver. The function responsible for receiving the API requests, in netserver, is recv_command:

undefined4 recv_command(netserver_session *session)
{
  [...]
  fd = bc_event_get_fd(*(undefined4 *)&session->field_0x30);
  max_body_read = 10;
  if (fd < 0) {
    [...]
  }
  else {
    received_data = session->received_data;
    while (session->header_len <= received_data) {
      session_data = session->data;
PARSE_DATA:
      data_size = session_data->data_size;
      if (0x9c40 < (int)data_size) {                                                                    [1]
        [... Invalid size ...]
      }
      if (0 < (int)data_size) {                                                                         [2]
        iVar2 = recv_body(session,fd,data_size);                                                        [3]
        [...]
      }
      max_body_read = max_body_read + -1;
      parse_recved_data(session);                                                                       [4]
      if (max_body_read == 0) {
        return 1;
      }
      received_data = session->received_data;
    }
    iVar2 = recv_header(session,fd);                                                                    [5]
    if (iVar2 != -2) {
      [...]
      session_data = session->data;
      goto PARSE_DATA;
    }
    [...]
}

At [5] is called the function responsible for receiving the header data. At [1] it is checked if the session_data->data_size, value inside the received header, is greater or equal than 0x9c40, in this case, the parsing is aborted because of the invalid size. Otherwise, another size check is performed at [2], checking if session_data->data_size is greater than 0. In this case, at [3], the recv_body is called. This function will receive the remaining part of the data. Eventually, at [4], the parse_recved_data is called.

The parse_recved_data function:

uint parse_recved_data(netserver_session *session)

{
  [...]
  if (session->maybe_parse_state == 0) {
    iVar1 = version_detect();
    if (-1 < iVar1) {
      return 0;
    }
    printf("session:%s version detect failed\n",session->client_ip);
    c_client_session::state_set(session,2);
  }
  else if (session->maybe_parse_state == 2) {
    session_data = session->data;
    if (session_data->magic == (session->cmd_list).magic) {
      node = (netserver_session_cmd_node *)
             c_client_session::cmd_init(session,session_data->data_size + 0x18,0);                      [6]
      if (node != (netserver_session_cmd_node *)0x0) {
        __src = session->data;
        node->status = NEW;
        node->command_type = 0;
        memcpy(node->recv_data,__src,node->recv_len);
        session->received_data = session->received_data - node->recv_len;
        node->cmd = session_data->cmd;
        c_client_session::cmd_add(session,node);                                                        [7]
        [...]
} This function, in some specific case, will be called twice for the same connection. The first time it will verify the correctness of some header data. The second time, calling the `cmd_init` function at `[6]`,  a `cmd` object is created.

The cmd_init function:

int ** c_client_session::cmd_init(netserver_session *session,uint recv_len,uint send_len)

{
  [...]

  puVar7 = &_mips_gp0_value;
  if ((0x9c58 < recv_len) || (0x9c58 < send_len)) {                                                     [8]
    printf("msg cmd send:%u or recv:%u len is too long!\n",send_len,recv_len);
    return (int **)0x0;
  }
  [...]
  if (recv_len != 0) {                                                                                  [9]
    __s = (netserver_session_data *)net_malloc(recv_len + 1);
    new_cmd_node->recv_data = __s;
    if (__s == (netserver_session_data *)0x0) {
      printf("func:%s line:%d msg init failed\n","cmd_init",0xc34);
      goto LAB_004557c4;
    }
    memset(__s,0,recv_len + 1);
    new_cmd_node->recv_len = recv_len;
  }
  [...]
}

The created cmd object has, in this specific case, a buffer field used to copy the header and the data received. At [8] it is checked that the provided sizes are not greater than 0x9c58. Then, at [9], if the recv_len parameter differs from zero the previous mentioned buffer is allocated using the recv_len parameter as size.

At [7] the created cmd is appended to the list of commands that will latter on be executed. This cmd is latter parsed in the parse_command_list function:

undefined4 parse_command_list(netserver_session *session)

{
  [...]

  cmd_node_cur = (session->cmd_list).node_base;
  do {
    if ((netserver_cmd_list *)cmd_node_cur == &session->cmd_list) {
      return 0;
    }
    node_data = cmd_node_cur->node_data;
    node_status = node_data->status;
    if (node_status == PROGRESS) {
      [...]
    }
    else {
      if (node_status == COMPLETED) {
       [...]
      }
      if (node_status == NEW) {
        netserver_session_data = node_data->recv_data;                                                  [10]
        if (netserver_session_data->cmd < 0x14e) {                                                      [11]
          [...]
      }
    }
    [...]
  } while( true );
}

The buffer allocated in the branch at [9] is, at [10], taken and dereferenced at [11] accessing one field of the copied header data.

An integer overflow exists at [6], indeed the session_data->data_size can be a negative integer number, this will bypass the invalid size branch at [1]. Indeed, it is checked if the data_size is greater than 0x9c40 as integer value, so a negative value will result in not taking that branch. If the provided data_size is 0xffffffe8, the sum at [6] will result in calling the cmd_init with the recv_len parameter equals to zero. Then the branch at [9] will not be taken and the buffer will not be allocated, leaving zero in the recv_data cmd’s field. Then at [11] this field will be dereferenced, but because it contains zero a null dereference will occur. This will result in the netserver binary crash and eventually in the reboot of the device.

Timeline

2022-01-14 - Vendor Disclosure
2022-01-19 - Vendor Patched
2022-01-26 - Public Release

Credit

Discovered by Francesco Benvenuto of Cisco Talos.