CVE-2022-21796
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.
Reolink RLC-410W v3.0.0.136_20121102
RLC-410W - https://reolink.com/us/product/rlc-410w/
9.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:H
CWE-20 - Improper Input Validation
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.
2022-01-19 - Vendor Disclosure
2022-01-19 - Vendor Patched
2022-01-26 - Public Release
Discovered by Francesco Benvenuto of Cisco Talos.