Talos Vulnerability Report

TALOS-2023-1703

Milesight MilesightVPN liburvpn.so create_private_key OS command injection vulnerability

July 6, 2023
CVE Number

CVE-2023-22371

SUMMARY

An os command injection vulnerability exists in the liburvpn.so create_private_key functionality of Milesight VPN v2.0.2. A specially-crafted network request can lead to command execution. An attacker can send a malicious packet 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 VPN v2.0.2

PRODUCT URLS

MilesightVPN - https://www.milesight-iot.com/milesightvpn/

CVSSv3 SCORE

8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H 9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H - chain: TALOS-2023-1702
##### CWE

CWE-77 - Improper Neutralization of Special Elements used in a Command (‘Command Injection’)

DETAILS

The MilesightVPN is a software that make easier the setup of VPN tunnel for the Milesight products and allow to monitor the connection status with a web server interface.

The MilesightVPN exposes the /Device_Auth API for the various Milesight devices for them to authenticate to the server and to get an OpenVPN configuration. The API is managed by the Device_Auth function:

function Device_Auth(res,postdata,connection){
    var authcode=postdata['authcode'],subnet=postdata['subnet'],\
                devicename=postdata['device_name'],sn=postdata['sn'];
    var newdt=dll_fun('get_openvpn_params',{});
    [...]
    if(newdt['result']['auth_code']!=authcode)
    {
        [...]
    }
    else
    {
        var $sql='select * from device where sn="'+sn+'"';
        connection.query($sql).then(function(data){
            if(data['error'])
            {
                [... error branch ...]
            }
            else
            {
                if(data['result'].length>0)
                {
                    [...]
                }
                else
                {
                    [...]
                    $sql1='insert into device(name,sn,remote_subnet,status) \
                    value("'+devicename+'","'+sn+'","'+subnet+'",0)';
                    connection.query($sql1).then(function(data1){
                        if(data1['error'])
                        {
                            [...]
                        }
                        else
                        {
                            var newdt3=dll_fun('register_client',{'cn':sn,'subnet':subnet});            [1]
                            [...]
                        }
                    })
                }
            }
        })
    }
}

This API expects four entries in the POST payload: - authcode: is a secret generated by the MilesightVPN - subnet: the subnet mask of the main network used by the device - device_name: the identifier of the device that is connecting to the MilesightVPN server - sn: is the serial number of the device that is connecting to the MilesightVPN server

Eventually the function will reach the code at [1] that will call a function of the liburvpn.so library. In this case the function called is liburvpn.so’s register_a_client that takes one JSON object as argument, the object has the sn and the subnet as entry values.

Following the liburvpn.so’s register_a_client function:

cJSON * register_a_client(cJSON *params)

{
  [... variable declaration ...]

  [... variable initialization ...]
  for (obj = params->child; obj != (cJSON *)0x0; obj = obj->next) {
    iVar1 = strcmp(obj->string,"cn");
    if (iVar1 == 0) {
      common_name = obj->valuestring;
    }
    else {
      iVar1 = strcmp(obj->string,"subnet");
      if (iVar1 == 0) {
        subnet = obj->valuestring;
      }
    }
  }
  if ((common_name == (char *)0x0) || (subnet == (char *)0x0)) {
   [...]
  }
  else {
    [...]
    type = check_common_name(common_name);
    if (type == 0) {
      device_max = get_clients_limit();
      snprintf(cmd,0x80,"echo \"device max:%d\" >> /var/urvpn.log",(ulong)(uint)device_max);
      system(cmd);
      print_timestamp();
      printf("Register a router(%s)\n",common_name);
      [...]
      update_subnet(common_name,subnet);
      ret = register_a_router(common_name,ip,mask);                                                     [2]
    }
    [...]
  }
  [...]
}

The function will perform various checks and eventually, if no error is encountered and the device is recognized as router, the function register_a_router, at [2], will be called. The register_a_router function will generate the cryptographic keys used for the VPN connection.

int register_a_router(char *cn,char *ip,char *mask)

{
  [...]
  iVar1 = general_keys("./urvpn/routers_ca",cn);
  return iVar1;
}

The register_a_router function will call the general_keys function, that is the one that is effectively going to generate the cryptographic keys:

int general_keys(char *path,char *name)

{
  [... variable declaration ...]


  is_name_0_string = strcmp(name,"000000000000");
  if (is_name_0_string == 0) {
    [...]
  }
  else {
    snprintf(public_name,0x200,"%s/%s.csr",path,name);
    snprintf(private_name,0x200,"%s/%s.key",path,name);
    snprintf(certificate_name,0x200,"%s/%s.crt",path,name);
  }
  private_name_path_exists = check_file_exist(private_name);                                            [3]
  if (private_name_path_exists == 0) {
    print_timestamp();
    printf("create file private %s\n",private_name);
    create_private_key(private_name);                                                                   [4]
  }
  [...]
}

This function will have as the name parameter the original sn, the serial number of the device. If this is not 000000000000, then various path-names are composed. We are going to focus on the private_name variable that is going to have the following form ./urvpn/routers_ca/<serial number>, at [3] it is checked if this pathname corresponds to an existing file, if this file does not exists the create_private_key function will be called at [4]:

void create_private_key(char *private_name)

{
 [...]
  snprintf(cmd,0x200,"openssl genrsa -out %s 1024",private_name);                                       [5]
  print_timestamp();
  printf("create private key:%s\n",cmd);
  system(cmd);
  [...]
}

This function, for the code path considered, will execute system("openssl genrsa -out ./urvpn/routers_ca/<serial number> 1024");. Because the serial number variable, originally sn and then called cn in the liburvpn.so library, is never checked until [5], the /Device_Auth API can lead to an OS command injection in liburvpn.so at [5]. An attacker would need to know the Authorization Code of the server to actually use the /Device_Auth API. But because TALOS-2023-1702 this information can be easily retrieved by an attacker.

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.