Talos Vulnerability Report

TALOS-2022-1451

Reolink RLC-410W netserver parse_command_list memory corruption vulnerability

January 26, 2022
CVE Number

CVE-2022-21796

Summary

A memory corruption vulnerability exists in the netserver parse_command_list functionality of reolink RLC-410W v3.0.0.136_20121102. A specially-crafted HTTP request can lead to an out-of-bounds write. An attacker can send an HTTP request 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

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

CWE

CWE-20 - Improper Input Validation

Details

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

The RLC-410W offers several APIs functionalities, once logged, through a binary called netserver. A specially crafted request to netserver can lead to write a null byte in a partially controllable address due to improper input validation.

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) {
        [... Invalid size ...]
      }
      if (0 < (int)data_size) {
        iVar2 = recv_body(session,fd,data_size);                                                        [1]
        [...]
      }
      max_body_read = max_body_read + -1;
      parse_recved_data(session);                                                                       [2]
      if (max_body_read == 0) {
        return 1;
      }
      received_data = session->received_data;
    }
    iVar2 = recv_header(session,fd);                                                                    [3]
    if (iVar2 != -2) {
      [...]
      session_data = session->data;
      goto PARSE_DATA;
    }
    [...]
} At `[3]` is called the function responsible for receiving the header data. If `data_size`, a value inside the received header, is greater than 0 and less or equal than 0x9c40, at `[1]`, the `recv_body` is called. This function will receive the remaining part of the data. Eventually, at `[2]`, the `parse_recved_data` is called:

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);                      [4]
      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);                                                        [5]
        [...]
} This function, in some specific case, is called twice for the same connection. The first time, calling `version_detect`, it will verify the correctness of the provided header and set some data based on the checks. The second time, calling the `cmd_init` function at `[4]`, a `cmd` object is created and appended, at `[5]`, to the list of commands that will latter on be executed.

The parse_recved_data:

undefined4
version_detect(netserver_session *session,undefined4 param_2,undefined4 param_3,void *param_4)

{
  [...]
  data = session->data;
  if (data->cmd != 1) {
    [... fail ...]
  }
  data_magic = data->magic;
  if (data_magic == 0xabcdef0) {
    session->choosen_magic = magic_0xabcdef0;
  }
  else {
    if (data_magic != 0xfedcba0) {
      [... fail ...]
    }
    session->choosen_magic = magic_0xfedcba0;
  }
  version_related = *(uint *)&data->encryption_type_related;
  (session->cmd_list).magic = data_magic;
  version_2 = version_related >> 24;
  [...]
  session->parse_state = 2;                                                                             [6]
  session->header_len = 0x18;                                                                           [7]
  [...]
}

This function will verify the correctness of the provided header and set some data based on the checks. One of the action performed is to distinguish between the request of a nonce, to perform a login, and a normal API request, allegedly, performed after the login. The parse_state, at [6], is changed to 2, this will allow to execute, in case of an API request, the second branch of parse_recved_data when called again. The header len is changed, at [7], to 0x18. Its original value was 0x14. This is because, in case of a normal API request, the header is extend by 4 bytes. This last header field represents the length of the provided API XML data provided.

Then the commands appended at [5] are parsed by 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 == COMPELTED) {
        [...]
      }
      if (node_status == NEW) {
        netserver_session_data = node_data->recv_data;
            [...]
            XML_LEN = &node_data->recv_data->xml_length;
            [...]
            if(XML_LEN < 0){                                                                            [8]
                [... ERROR ...]
            }
            [...]
            end_of_xml = &node_data->recv_data->xml_data + XML_LEN;                                     [9]
            xml_end_byte = (int)*end_of_xml;                                                            [10]
            *end_of_xml = '\0';                                                                         [11]
            [...]
      }
    }
    [...]
  } while( true );
}

Eventually the command will reach the parse_command_list function and at [9] the XML length, field in the header, will be used to seek the last byte of the XML data. This byte pointer, at [10], is dereferenced and then at [11] a null byte is placed in that position.

Since the only relevant check performed on the provided XML length is at [8], checking if the value is lower than 0, it is possible to write a null byte in a partially controllable heap address. Indeed, the value of the XML length, with positive value, is totally controllable, allowing partial control of the heap address, calculated at [9], where the null byte is placed.

Timeline

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

Credit

Discovered by Francesco Benvenuto of Cisco Talos.