CVE-2021-40404
An authentication bypass vulnerability exists in the cgiserver.cgi Login functionality of reolink RLC-410W v3.0.0.136_20121102. A specially-crafted HTTP request can lead to authentication bypass. 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/
5.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
CWE-284 - Improper Access Control
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 enforces that the only API usable as a not-logged-in user is the Login
API. It detects if the provided command is Login
by checking the URL’s cmd
parameter. The cgiserver.cgi
binary accepts a list of commands provided as an array of JSON objects in the body as the actual commands; this can lead to a bypass of the “please login first” check.
The cgiserver.cgi
manages the API requests parsing the commands and parameters provided. One way to issue commands and parameters is by providing those in a JSON array in the body. The commands looks like the following:
[
{
"cmd": <COMMAND NAME 1>,
"action": <ACTION NUMBER 1>,
"param":{
<COMMAND PARAMETERS 1>
}
},
...
{
"cmd": <COMMAND NAME n>,
"action": <ACTION NUMBER n>,
"param":{
<COMMAND PARAMETERS n>
}
},
]
The parse_incoming_and_check_command
function parses the incoming request:
int parse_incoming_and_check_command(cgi_request *req)
{
[...]
Json::Reader::Reader(json_reader);
Json::Value::Value(&json_value,0);
iVar1 = parse_request(req);
if (iVar1 == 0) {
if (((int)req->CONTENT_LENGTH < 1) || (req->is_commands_in_body == 0)) {
/* no body is present */
[...]
}
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)post_data_as_basic_string,(allocator *)req->body_data);
pbVar4 = post_data_as_basic_string;
post_data_is_valid_json =
Json::Reader::parse(json_reader,post_data_as_basic_string,&json_value,true);
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string
((basic_string<char,std::char_traits<char>,std::allocator<char>> *)
post_data_as_basic_string);
if (post_data_is_valid_json == 0) {
AVar3 = param error;
}
else {
post_data_is_valid_json = Json::Value::isArray(&json_value,pbVar4);
json_idx = 0;
if (post_data_is_valid_json != 0) {
for (; total_number_of_elements = Json::Value::size(&json_value),
json_idx < total_number_of_elements; json_idx = json_idx + 1) {
[... parse a JSON command object and insert it into the command list ...] [1]
}
goto LAB_0043ccbc;
}
AVar3 = protocol;
}
req->req_status = AVar3;
}
iVar1 = -1;
LAB_0043ccbc:
Json::Value::~Value(&json_value);
Json::Reader::~Reader(json_reader);
return iVar1;
}
At [1]
, one at a time, the JSON commands are parsed and inserted into a command list. Then, if no username parameter is provided in the URL, the associate_session_to_request
function is executed:
undefined4 associate_session_to_request(c_cgiserver_obj *cgi,cgi_request *req)
{
dword dVar1;
API_status_code AVar2;
cgi_session *session_;
int iVar3;
cgi_session *session;
session_node *session_node_cur;
dword apStack56 [4];
session_node_cur = (cgi->session_node).session_node_start;
while( true ) {
if (session_node_cur == (session_node *)&(cgi->session_node).session_node_end) {
AVar2 = please login first;
if (req->req_command_ID == Login) { [2]
if (cgi->number_of_active_sessions < cgi->max_number_of_sessions) {
session_ = (cgi_session *)operator.new(0xe8);
/* try { // try from 0043c5b4 to 0043c5bb has its CatchHandler @ 0043c644 */
c_cgisession::c_cgisession(session_,cgi,cgi->next_session_ID);
req->session_ID = session_->session_ID;
iVar3 = c_cgisession::init(session_,req);
if (-1 < iVar3) {
cgi->next_session_ID = cgi->next_session_ID + 1;
apStack56[2] = session_->session_ID;
apStack56[3] = (dword)session_;
std::
_Rb_tree<unsigned_int,std::pair<unsigned_int_const,c_cgisession*>,std::_Select1st<std::pair<unsigned_int_const,c_cgisession*>>,std::less<unsigned_int>,std::allocator<std::pair<unsigned_int_const,c_cgisession*>>>
::_M_insert_unique((pair *)apStack56,&cgi->session_node,(dword)(apStack56 + 2));
c_cgisession::cgi_req_proc(session_,req); [3]
return 0;
}
[...]
}
This function aims to bind the incoming request with an existing session or create a new one if the command is Login
. At [2]
the cmd
parameter, provided in the URL, is checked against Login
. If the command is Login
a new session is created, and then the session and the request are passed as arguments, at [3]
, to the cgi_req_proc
function that then calls the proper requested APIs.
The cgi_req_proc
function:
undefined4 __thiscall c_cgisession::cgi_req_proc(cgi_session *session,cgi_request *req)
{
[...]
for (cmd_node_cursor = req->cgi_cmd_node_base->cmd_node_start;
cmd_node_cursor != (cgi_cmd_node *)&req->cgi_cmd_node_base->cmd_node_end;
cmd_node_cursor =
(cgi_cmd_node *)std::_Rb_tree_increment((_Rb_tree_node_base *)cmd_node_cursor)) {
cgi_cmd = cmd_node_cursor->cgi_cmd;
if (cgi_cmd->HTTP_status_code == OK) {
if ((cgi_cmd->API_processing_status & 0xfffffffd) == 0) {
cgi_cmd->API_processing_status = 1;
command_struct = cgi_find_cmd_table(cgi_cmd->command_ID);
if (command_struct != (command_struct *)0x0) {
[... some log print ...]
API_function = command_struct->API_function;
API_result = (*API_function)(session,cgi_cmd); [4]
if ((API_result != 0) || (cgi_cmd->HTTP_status_code != OK)) {
if (cgi_cmd->HTTP_status_code == OK) {
cgi_cmd->HTTP_status_code = protocol;
}
[... some log print ...]
cgi_cmd->API_processing_status = 3;
}
}
}
}
[...]
}
return 0;
}
All the commands parsed at [1]
are iterated in cgi_req_proc
, and if the provided command name is valid, at [4]
, the corresponding API function is executed.
The command list is populated at [1]
regardless of the URL’cmd
parameter value. In the specific case of the URL cmd=Login
, if no username parameter is provided in the URL, the cgi_req_proc
can be called with an arbitrary list of commands, which can be different from the Login
one. This will lead to execution, for every command specified in the request body, of the actual API code.
For example, considering the URL cmd
parameter equals to Login
, it would be possible to send a body like the following:
[
{
"cmd": "Upgrade",
"action": 0,
"param": {}
}
]
With the above command body, and the URL cmd=Login
, it would be possible to reach the Upgrade
API code.
Note that, the session
struct contains a table with the permitted API. This table is populated after the Login
API is executed with valid credentials. Because the process explained above exploits not going through the login process, there are no permissions for the session.
For instance, the relevant part of the Upgrade
API:
undefined4 Upgrade(cgi_session *session,cgi_cmd *cmd)
{
[...]
if (cmd->parsing_status == NOT_HANDLED) {
error_code = cgi_check_ability(cmd->command_ID,session,0); [5]
if (error_code != NO error) { [6]
[...]
cmd->HTTP_status_code = error_code;
cmd->associated_request->perform_reboot = 1;
return 0xffffffff;
}
cmd->parsing_status = PARSE_OK;
}
[...]
}
This code should not be reached for the not-logged-in users, but because of the problem explained above it is possible to reach the Upgrade
code with an invalid session
. At [5]
the permission required is checked against the sesssion
permissions. Because of the check at [6]
it is not possible to complete the Upgrade
API.
This vulnerability in combination with TALOS-2021-1421 leads to the reboot of the camera without authentication. This vulnerability in combination with TALOS-2021-1422 leads to the reboot of the camera without authentication. This vulnerability in combination with TALOS-2021-1425 leads to the execution of several APIs without authentication:
{'Login', 'HeartBeat', 'GetMdState', 'GetHddInfo', 'Unknown', 'Playback', 'UpgradePrepare', 'Format', 'SetMdAlarm', 'GetWifiSignal', 'GetAbility', 'GetMdAlarm', 'Logout'}
2021-12-06 - Vendor Disclosure
2022-01-19 - Vendor Pathed
2022-01-26 - Public Release
Discovered by Francesco Benvenuto of Cisco Talos.