Talos Vulnerability Report

TALOS-2022-1445

Reolink RLC-410W device TestEmail out-of-bounds write vulnerability

January 26, 2022
CVE Number

CVE-2022-21217

Summary

An out-of-bounds write vulnerability exists in the device TestEmail functionality of reolink RLC-410W v3.0.0.136_20121102. A specially-crafted network 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.1 - CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-457 - Use of Uninitialized Variable

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 the possibility to send motion-alert e-mail notifications. This API is only usable with admin privileges. Is possible to use the TestEmail API to verify that the configuration is done properly sending a test e-mail. A buffer overflow during the execution of the TestEmail API exists due to the use of uninitialized variable. This can lead to an out of bound write vulnerability.

The TestEmail API accepts the following JSON data format:

[
    {
        "cmd": "TestEmail",
        "action": 0,
        "param": {
            "Email": {
                "smtpServer": "<SERVER_NAME>",
                "smtpPort": <PORT>,
                "userName": "<USERNAME>",
                "password": "<PASSWORD>",
                "nickName": "<NICKNAME>",
                "ssl": 0,
                "interval": "<INTERVAL>",
                "addr1": "<RECIPIENT_1>",
                "addr2": "<RECIPIENT_2>",
                "addr3": "<RECIPIENT_3>"
            }
        }
    }
]

Firstly the request go through the cgiserver.cgi’s TestEmail_API function:

undefined4 TestEmail_API(cgi_session *session,cgi_cmd *cmd_node)

{
  [...]

  pcVar4 = session->cgi_obj;
  test_mail_api_data = (smtp_test_mail_form_data_cgiserver *)cmd_node->paramt_data;
  if (cmd_node->parsing_status == NOT_HANDLED) {
   [...]
  }
  else {
LAB_00414728:
    if (cmd_node->parsing_status == PARSE_OK) {
      [...]
      cgi_log(0,2,"cgi_cmd_test_email, in stage 2");
      test_mail_struct.ssl = test_mail_api_data->ssl;
      test_mail_struct.port = test_mail_api_data->port;
      strncpy(test_mail_struct.smtp_server,test_mail_api_data->smtp_server,0x7f);                       [1]
      strncpy(test_mail_struct.username,test_mail_api_data->username,0x7f);
      strncpy(test_mail_struct.password,test_mail_api_data->password,0x7f);
      strncpy(test_mail_struct.nickname,test_mail_api_data->nickname,0x7f);
      strncpy(test_mail_struct.addr1,test_mail_api_data->addr1,0x7f);
      strncpy(test_mail_struct.addr2,test_mail_api_data->addr2,0x7f);
      strncpy(test_mail_struct.addr3,test_mail_api_data->addr3,0x7f);
      iVar2 = cgi_send_msg(session,cmd_node,INTL_APP_ID,SRVM_MODULE_ID,MLTS_FUNC_ID,
                           &test_mail_struct,0x388,cgi_msg_callback,28000);                             [2]
      [...]
  }
  [...]
}

This function prepare the provided API data into a structure. This struct is going to be received, through an IPC mechanism started at [2], by the device binary. This binary is responsible to receive the provided API data and execute the TestEmail API. The provided data will eventually reach the smtp_send_email_wrap function:

undefined4 smtp_send_email_wrap(void)

{
  [...]

  memset(rcpt_string_normal,0,0x200);
  memset(rcpt_string_angular_brackets,0,0x200);
  [...]
  if (IPC_struct != (smtp_data_from_cgi *)0x0) {
    write_msmtprc(&IPC_struct->test_email_data);
    some_len = strlen(&IPC_struct->field_0x394);
    if (some_len < 0x80) {
      mail_content = &IPC_struct->mail_content;
      mail_content_len = strlen((char *)mail_content);
      if ((mail_content_len < 0x100) &&
         (res_concat = concatenate_recipients
                            (&IPC_struct->test_email_data,
                             rcpt_string_normal,0x200,rcpt_string_angular_brackets,0x200),              [3]
         -1 < res_concat)) {
        [...]
         }
    [...]
}

In smtp_send_email_wrap the concatenate_recipients function, at [3], is called. This function will concatenate the different recipients provided: addr1, addr2 and addr3, into a single string.

The concatenate_recipients function:

undefined4
concatenate_recipients
          (smtp_test_mail_form_data *testemail_data,char *recpt_normal,size_t size_1,
          char *recpt_with_angular_brackets,size_t size_2)

{
 [...]

  uVar1 = 0xfffffa21;
  if ((((testemail_data != (smtp_test_mail_form_data *)0x0) && (recpt_normal != (char *)0x0)) &&
      (recpt_with_angular_brackets != (char *)0x0)) && ((0 < (int)size_1 && (0 < (int)size_2)))) {
    addr_string = testemail_data->addr1;
    addr_N = 0;
    buff_2_ = recpt_with_angular_brackets;
    buff_1_ = recpt_normal;
    do {
      if (*addr_string != '\0') {
        addr_len = strlen(addr_string);
        if (addr_N == 0) {
          snprintf(buff_1_,size_1,"%s",addr_string);                                                    [4]
          snprintf(buff_2_,size_2,"<%s>",addr_string);
          buff_2_new_offset = addr_len + 2;
          buf_1_consumed_size = -addr_len;
          size_2 = (size_2 - addr_len) - 2;
         buf_1_new_offset = addr_len;
        }
        else {
          snprintf(buff_1_,size_1," %s",addr_string);                                                   [5]
          snprintf(buff_2_,size_2,",<%s>",addr_string);
          buf_1_new_offset = addr_len + 1;
          buf_1_consumed_size = -addr_len -1;
          buff_2_new_offset = addr_len + 3;
          size_2 = (size_2 - addr_len) - 3;
        }
        buff_1_ = buff_1_ + buf_1_new_offset;                                                           [6]
        size_1 = size_1 + buf_1_consumed_size;                                                          [7]
        buff_2_ = buff_2_ + buff_2_new_offset;
      }
      addr_N = addr_N + 1;
      addr_string = addr_string + 0x80;
    } while (addr_N != 3);
    uVar1 = 0xfffffa0f;
    if ((*recpt_normal != '\0') && (uVar1 = 0xfffffa0f, *recpt_with_angular_brackets != '\0')) {
      uVar1 = 0;
    }
  }
  return uVar1;
}

This function fills, at the same time, both the recpt_normal and recpt_with_angular_brackets buffers. Both are stack buffers of 0x200 characters. Let us consider only how the recpt_normal is filled. At [4] or [5] is used the snprintf function to insert one of the recipient into the buff_1_ buffer, using at most size_1 characters. The buff_1_ is a cursor of the recpt_normal that is moved after each snprintf at [6]. The size_1 represents how many bytes are left into the buff_1_ buffer. This variable starts with the value equal to the size of the buffer. In this case, 0x200 as value. Then after each snprintf the length of the recipient plus the possible space character, used at [5], are detracted, at [7], from size_1. Because each recipient has at most 127 characters the maximum amount of bytes, considering also the spaces used at [6], that can be placed in the recpt_normal buffer is 0x180.

The concatenate_recipients function assumes that the provided data are correctly null terminated. Because the TestEmail_API function stores the data, that will be used by the concatenate_recipients function, into stack buffer without initializing those, the assumption that the provided data are null terminated is not always true. This can lead to a stack based buffer overflow.

The TestEmail_API function copies the API data using strncpy with a size of 0x7f that is exactly 127. So, if the provided string has as size 127 or more the null terminator will not be placed.

The API data structure has the following layout:

offset    size    type        name
0x0       0x80    char[128]	  smtp_server	
0x80      0x4	  dword	      ssl	
0x84      0x4     dword	      port	
0x88      0x80    char[128]	  username	
0x108     0x80    char[128]	  password	
0x188     0x80    char[128]	  nickname	
0x208     0x80    char[128]	  addr1	
0x288     0x80    char[128]	  addr2	
0x308     0x80    char[128]	  addr3	

The memory layout data, starting from the test_mail_struct variable, at [1] is:

0x7fddb900│+0x0000: 0x7fddb900
[...]
0x7fddbb08│+0x0208: 0x0053bc60  →  0x0053bbe8  →  0x0053c4d0  →  0xffffffff
[...]
0x7fddbb84│+0x0284: 0x7fddbba8  →  0x0053c548  →  0x005512b8  →  0xffffffff
0x7fddbb88│+0x0288: 0x00000000
[...]
0x7fddbc04│+0x0304: 0x76f4e4fc  →  <free+88> lw gp, 16(sp)
0x7fddbc08│+0x0308: 0x7711f8e4  →  <pthread_mutex_unlock+0> lui gp, 0x2
[...]
0x7fddbc84│+0x0384: 0x00551348  →  0x00000000
0x7fddbc88│+0x0388: 0x7fddbd18  →  0x0053c4f8  →  0x0053c540  →  0xffffffff

The addr1 starts at offset +0x0208 and will end at offset +0x287. The last byte of the addr1 buffer is at offset +0x287 where there is, before the strncpy, the character 7f. If the provided addr1 string has 127 character no null terminator will appear but instead the 7f will follow the 127 characters. Is worth to note that, because the address 0x7fddbba8 at offset +0x0284 reference a stack address, the position at offset +0x287 will, allegedly, always contains a value similar to 7f and anyway different than 0. The same is true for addr2 considering the range +0x0288 to +0x307.

So if we take for instance a TestEmail that has as addr1 and addr2 strings with 127 characters and addr3 with 100 characters, in concatenate_recipients the second time that size_1 is updated, at [7], we will have:

initial_size = 512
addr1_len = (128 + 128 + 100)
addr2_len_with_space = (128 + 100 + 1)
size_1 = initial_size - addr1_len - addr2_len_with_space = -73

-73, if considered as unsigned of 4 bytes, will have a value of 4294967223. And buff_1_ will have an offset of +585 from the start of recpt_normal. So the next snprintf, at [5], will be called with a position outside the boundary of the recpt_normal buffer with an enormous size, leading to a stack-based buffer overflow with the data of addr3.

Crash Information

do_page_fault(): sending SIGSEGV to dev_main for invalid read access from 43434342
epc = 43434343 
ra  = 43434343

Timeline

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

Credit

Discovered by Francesco Benvenuto of Cisco Talos.