CVE-2023-22371
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.
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
MilesightVPN - https://www.milesight-iot.com/milesightvpn/
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’)
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.
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
2023-02-14 - Initial Vendor Contact
2023-02-21 - Vendor Disclosure
2023-07-06 - Public Release
Discovered by Francesco Benvenuto of Cisco Talos.