Talos Vulnerability Report

TALOS-2023-1712

Milesight UR32L vtysh_ubus _get_fw_logs OS command injection vulnerability

July 6, 2023
CVE Number

CVE-2023-22299

SUMMARY

An OS command injection vulnerability exists in the vtysh_ubus _get_fw_logs functionality of Milesight UR32L v32.3.0.5. A specially crafted network request can lead to command execution. An attacker can send a network request to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Milesight UR32L v32.3.0.5

PRODUCT URLS

UR32L - https://www.milesight-iot.com/cellular/router/ur32l/

CVSSv3 SCORE

8.8 - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-78 - Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)

DETAILS

The Milesight UR32L is an industrial cellular router. The router features include support for multiple VPNs, a router console shell, firewall and many others.

The Milesight router offers several functionalities through the /cgi endpoint. The “core” functionality we are considering is called yruo_debug_firewall. In this “core” there is a function called “get”. The function that manages this functionality is the vtysh_ubus’s fw_logs_get function:

void fw_logs_get(undefined4 param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4,
                undefined4 *data)

{
  [... variable declaration ...]
  
  [... variable initialization ...]
  json_msg_output("!! yruo_fw_logs.get params",data);
  blob_buf_init(b,0);
  data_len = __bswapsi2(*data);
  blobmsg_parse(fw_logs_get_policy,2,tb,data + 1,(data_len & 0xffffff) - 4);                            [1]
  if (tb[0] != (blob_attr *)0x0) {
    base_value_ptr = (char *)blobmsg_get_string(tb[0]);
    strncpy(tb_base_value,base_value_ptr,0x20);
    system_ptr = (system_base_struct *)get_handler_bybase(&debug_bases,1,tb_base_value);                [2]
    if (system_ptr != (system_base_struct *)0x0) {
      switch_to_enable_node();
      _get_fw_logs = (code *)system_ptr->get_func;
      if ((_get_fw_logs != (code *)0x0) &&
         (_get_fw_logs = (code *)(*_get_fw_logs)(tb,"get"), _get_fw_logs == (code *)0x0))               [3]
      [...]
        [...]
      }
    }
  }
  [...]
}

The data are transmitted through blobmsg structures. The two variable that are parsed into the tb array, at [1], are: - base: this value must be equal to “firewall_log”, otherwise no functionality will be performed. The value of this data will be parsed in the tb[0] variable. - command: this value will be used as argument of the iptables shell command. The value of this data will be parsed in the tb[1] variable.

At [2] the base value will be used to get a struct handler that in the code is called system_ptr. Because there is only, in this case, one valid value for base, we know the struct fetched has as value of the system_ptr->get_func the _get_fw_logs function pointer. At [3] the _get_fw_logs function will be called:

void _get_fw_logs(blob_attr **tb, char* type)

{
  [... variable declaration ...]

  [... variable initialization ...]
   = (undefined4 *)zcalloc(1,8);
  if (two_word != (undefined4 *)0x0) {
    command = tb[1];                                                                                    [4]
    [...]
    if ((command != (blob_attr *)0x0) &&
       (command_string = (char *)blobmsg_get_string(command), *command_string != '\0')) {
      dup_command = zstrdup(1,command_string);
      [...]
      command_string = (char *)(dup_command + -1);
      do {
        command_string = command_string + 1;
        command_string_cursor = *command_string;
        if (command_string_cursor == '\0') {
          snprintf(iptables_command,0x100,"iptables -w %s",dup_command);                                [5]
          zlog_debug("exec fw command(%s)\n",iptables_command);
          __stream = popen(iptables_command,"r");                                                       [6]
          [...]
          }
          [...]
        }
      } while (command_string_cursor != '&' && command_string_cursor != ';');
      [...]
    }
  }
  [...]
} This function takes as first argument the `fw_logs_get`'s `tb` variable pointer. Then, at `[4]`, `tb[1]` is used to fetch the `command` argument sent in the `/cgi` API. The `command` variable is used, at `[5]`, to compose the `iptables -w <command>` string. This is then used at `[6]` as the argument for the `popen` function, effectively executing the  `iptables -w <command>` shell command.

The payload for the /cgi API to execute the “get” function in the “yruo_debug_firewall” core would look likes this:

{
"id":60,
"execute":10,
"core":"yruo_debug_firewall",
"function":"get",
"values":[
    {
        "base":"firewall_log",
        "command":"-S",
        }
    ]
}

This would execute the iptables -W -S command listing all the iptables rules.

Because no exhaustive checks are performed on the command parameters until it reaches the popen function, this leads to an OS command injection vulnerability.

VENDOR RESPONSE

Since the maintainer of this software did not release a patch during the 90 day window specified in our policy, we have now decided to release the information regarding this vulnerability, to make users of the software aware of this problem. See Cisco’s Coordinated Vulnerability Disclosure Policy for more information: https://tools.cisco.com/security/center/resources/vendor_vulnerability_policy.html

TIMELINE

2023-02-14 - Initial Vendor Contact
2023-02-21 - Vendor Disclosure
2023-07-06 - Public Release

Credit

Discovered by Francesco Benvenuto of Cisco Talos.