Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chain

  • Cisco Talos discovered 17 vulnerabilities (63 CVEs) in the Milesight UR32L router and five vulnerabilities (six CVEs) in the Milesight MilesightVPN remote access solution software.
  • An attacker could exploit the vulnerabilities discovered to completely compromise the UR32L and MilesightVPN.
  • This post presents an attack scenario in which the UR32L is only reachable through the MilesightVPN remote access solution. The blog explains how an attacker could exploit the MilesightVPN and then fully compromise the UR32L.
Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chain

Cisco Talos recently discovered several vulnerabilities in Milesight‘s UR32L – an ARMv7 Linux-based industrial cellular router — and Milesight’s MilesightVPN, a remote access solution for Milesight devices.

In all, Cisco Talos is releasing 22 security advisories today, nine of which have a CVSS score greater than 8, associated with 69 CVEs. Talos is disclosing these vulnerabilities despite no official fix from Milesight, in adherence to Cisco’s vulnerability disclosure policy. Milesight did not respond appropriately during the 90-day period as outlined in the policy.

The MilesightVPN is the remote access solution for Milesight devices not directly exposed to the internet. The MilesightVPN will perform this through a VPN system, making the VPN setup for the devices and the administrator easy. The MilesightVPN provides an HTTP admin page to monitor devices’ tunnel connection to the MilesightVPN itself and generate VPN configuration to join devices’ VPNs. By default, the MilesightVPN binds the HTTP server ports and the one VPN port on all interfaces. This software must be installed on a server machine reachable by the administrator and devices. Because of this, the most popular setup for administrator is to expose the MilesightVPN to the internet.

The Milesight UR32L is an industrial router that offers cellular capabilities. The router supports multiple users with different permissions. It offers a shell with restricted capabilities access. And many common industrial router features and functionalities. The Milesight UR32L does not include the ability to obtain and act with root privileges.

Attack scenario overview

In this blog post, Talos will present an attack scenario in which an adversary targets the Milesight UR32L, where the router is not exposed to the internet and instead uses a VPN tunnel for providing access to its internal network and the router itself. We considered the scenario where the device is managed through MilesightVPN. This software must be installed on a machine that is reachable by the UR32L router and the administrator. For the purposes of this post, we assume the MilesightVPN server is exposed to the internet.

MilesightVPN uses OpenVPN as the underlying VPN system. It is an excellent choice to use a well-established VPN system. OpenVPN is a well-known, tested VPN technology, which means it is less likely to have major security issues. But the MilesightVPN creates services around the OpenVPN tunnel like an HTTP server to monitor the connections and to generate OpenVPN configuration for joining the device’s VPN. By default, the MilesightVPN binds the HTTP server ports and the OpenVPN one on all interfaces. So, if the MilesightVPN is accessible on the internet, those services are by default accessible, too.

The UR32L has its own HTTP server; which allows users to manage the router configuration. We are also assuming the UR32L’s HTTP server is only accessible from the VPN and the MilesightVPN server is the only node that can generate a VPN configuration.

Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chainAn illustration of the proposed attack scenario.

The graphic below includes only the vulnerabilities relevant to this proposed attack scenario. This graphic shows the steps that an attacker could take to obtain root access to a Milesight UR32L:

Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chain

Attack walkthrough

After locating the IP address of the MilesightVPN server, the attacker still cannot log in to the HTTP admin page due to the lack of valid credentials. The HTTP server login page of MilesightVPN looks like this:

Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chain

From this point, an attacker could exploit TALOS-2023-1701 (CVE-2023-22319) to bypass the login and access the admin web pages on MilesightVPN. This vulnerability is a SQL injection in the LoginAuth function responsible for checking the provided credentials. The check uses SQL without a prepared statement or any form of sanitization. The LoginAuth code is shown below:

    function LoginAuth(res,postdata,connection){ 
        console.info('#######log.node:loginauth start'); 
        var sha512=crypto.createHash('sha512'); 
        sha512.update(postdata.pwd); 
        var pwd=sha512.digest('hex'); 
[1]     $sql="select * from user where user='"+postdata.user+"' and passwd='"+pwd+"'"; 
[2]     connection.query($sql).then(function(data){ 
            var result={}; 
            if(data['error']) 
            { 
                [...] 
            } 
            else 
            { 
                if(data['result'].length>0) 
                { 
                    var dt=data['result']; 
                    result['status']=1; 
                    var token=generateToken(dt[0]['user']); 
                    var exp=new Date(
                        new Date().getTime()+
                            expiretime*1000).toUTCString(); 
                    res.setHeader('Set-Cookie',['token='+token]); 
                    console.info('#######log.node:loginauth success'); 
                    res.write(JSON.stringify(result)); 
                    res.end(); 
                } 
                else 
                { 
                    [...] 
                } 
            } 
        }); 
    } 

At [1], the function composes, the SQL query for checking if the username and password provided correspond to the one of an existing user. Then, at [2], the query is executed, if the resulting table is not empty a JWT, corresponding to the first matched user, is crafted and placed in the response header as the value of Set-Cookie. This function is vulnerable to a SQL injection vulnerability because the "preparation" of the query string is performed through string concatenation instead of a prepared statement. This SQL injection can allow an attacker to bypass the authentication of the HTTP server and gain admin access.

After bypassing the login page, an attacker would be presented with the following interface. The example below includes an interface with multiple devices registered and a single device that has an active connection.

Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chain

From here, an attacker can glean information about the devices connected to the server and their IPs. Furthermore, it is possible to obtain an OpenVPN configuration file to join the VPN tunnel and communicate with the devices that were previously unreachable. The attacker can now communicate with all devices in the VPN. The following image is the HTTP server login page of the Milesight UR32L router:

Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chain

From here, several attack paths are possible. TALOS-2023-1697 (CVE-2023-23902) is a pre-authentication stack-based buffer overflow that can lead to arbitrary remote code execution (RCE).

The attacker's only requirement is to be able to communicate with the router's HTTP server. This vulnerability is a pre-authentication, stack-based buffer overflow in the decrypt_string function of the uhttpd binary, the UR32L’s HTTP server binary. This function is responsible for decrypting the login password provided for the webpage login. The password sent from the browser is first AES-encrypted and then Base64-encoded. The decrypt_string will Base64 decode and then AES decrypt the password.

The uhttpd binary is not a Position Independent Executable (PIE), meaning that the binary is always loaded at the same virtual memory addresses, but the libraries are Position Independent Code (PIC). This makes it possible for an attacker to perform a code reuse attack using the binary code without any information leaks unless the attacker needs to reuse some of the libraries' code. Furthermore, the code reuse attack scenario is the path of least resistance for exploitation because no stack canary is used in this binary.

The binary is executed as root and makes use of functions such as system and popen, as such, one of the options would be to mount an attack aiming to execute OS commands.

The following is a code snippet of the uhttpddecrypt_string:

void decrypt_string(
    char *b64_encrypted_password,
    char *decrypted_password,
    size_t size_decrypted_password)
{ 
    [...] 
    uchar stack_decrypted_string [72]; 
    [... init the AES_key variable] 
    [... init the AES_IV variable] 
    [... calculate the __size variable value ...] 
    base64_decoded_string_start = (uchar *)malloc(__size); 
    [...] 
    memset(base64_decoded_string_start,0,__size); 
    processed_len = 0; 
    base64_decode_string_cursor = base64_decoded_string_start; 
    do { 
        // this check allow to base64 decode the string before decrypting it 
        if ((password_len_ - padding) <= processed_len) { 
            *base64_decode_string_cursor = '\0'; 
            ctx = EVP_CIPHER_CTX_new(); 
            [... check error ...] 
            cipher_type = EVP_aes_128_cbc(); 
            processed_len = EVP_DecryptInit_ex(
                ctx,
                cipher_type,
                (ENGINE *)0x0,
                AES_key,
                AES_IV); 
            [... check error ...] 
[1]         processed_len = EVP_DecryptUpdate(
                ctx,
                stack_decrypted_string,
                &output_len,
                base64_decoded_string_start,
                (base64_decode_string_cursor + 
                    (-1 - base64_decoded_string_start)));                              
            [... check error ...] 
            processed_len = output_len; 
            iVar3 = EVP_DecryptFinal_ex(
                ctx,
                stack_decrypted_string + output_len,&output_len); 
            [... check error ...] 
            processed_len = processed_len + output_len; 
            EVP_CIPHER_CTX_free(ctx); 
            stack_decrypted_string[processed_len] = '\0'; 
            EVP_cleanup(); 
            ERR_free_strings(); 
            free(base64_decoded_string_start); 
[2]         strncpy(
                decrypted_password,
                (char*)stack_decrypted_string,
                size_decrypted_password);                 
            return; 
        } 
        [...] 
[3]    	[... base64 decode ...]                 
    } while( true ); 
} 

This function has three parameters: the b64_encrypted_password parameter is the AES-encrypted and Base64-encoded password string that will be Base64 decoded and then AES decrypted; decrypted_password is the destination buffer where the decoded and decrypted password will be copied;  size_decrypted_password is the size of the decrypted_password buffer. At [3] the b64_encrypted_password string is Base64 decoded and then, at [1], the decoded value is AES decrypted into stack_decrypted_string, a 72-byte long stack buffer. Eventually, at [2], size_decrypted_password bytes will be copied from the stack_decrypted_string buffer into the decrypted_password buffer.

In the decrypted_password only size_decrypted_password bytes will be copied, which will prevent any type of buffer overflow in the decrypted_password buffer.

At [1] the OpenSSL's EVP_DecryptUpdate function will perform the AES decryption of the Base64 decoded user-controlled data into the stack_decrypted_string stack buffer. Because the stack_decrypted_string stack buffer is fixed in size and the decrypted string, provided by a user, can be greater in length than the stack buffer. This can lead to a buffer overflow in the stack_decrypted_string buffer.

Essentially, the login API accepts the password as the Base64 encoding of the AES encryption of the password content. This function is used to decode the Base64 string and then AES decrypts it to obtain the actual password value.  The vulnerability is that the password can overflow a stack buffer overwriting the stack content, including the stored return address.

The exploitation of this vulnerability becomes easier because of the epilogue of this function:

  strncpy(
      decrypted_password,
      (char*)stack_decrypted_string,
      size_decrypted_password);          
  return; 

The decrypted_password is a buffer in the function caller stack space where the decoded and decrypted password is saved. In assembly, this looks like:

ldr r0, [sp,#0xc]  
bl strncpy  
add sp, sp, #0x84  
pop {r4,r5,r6,r7,r8,r9,r10,r11,pc} 

The function that copies the plain text password into the decrypted_password array, which is also the last function call before returning to the caller, is strncpy. The r0 register is used for passing the first argument of the function; in the case of strncpy, r0 points to the destination buffer. After the strncpy function call r0 will point to the beginning of the plaintext password.

The fact that the vulnerability considered is a stack buffer overflow, the binary does not have a stack canary, the binary is not PIE and uses the system function in combination with the fact that we control the content of what r0 points to, makes the exploitation straightforward. An attacker could overwrite the return address making the function return to the system function and controlling the executed shell command by placing the command to execute at the beginning of the plaintext password payload, as shown in the gdb screenshot below.

Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chain

The pc is the last instruction of the decrypt_string function and the return address was overwritten with the system plt entry, know because the binary is not PIE, and the r0 registry points to the controllable string. So, the decrypt_string returning will execute system(“controllable”).

Vulnerability Details

The following vulnerabilities are command injection issues in Milesight UR32L. These vulnerabilities exist in different functionalities of the router. An attacker could exploit these vulnerabilities by sending a specially crafted network packet to a targeted device:

There are also several vulnerabilities in the UR32L that could lead to a buffer overflow. An attacker could trigger these vulnerabilities by sending a specially crafted HTTP request or network request to the targeted device, depending on the specific vulnerability.

TALOS-2023-1705 (CVE-2023-23546) is a misconfiguration vulnerability in the Milesight UR32L that could lead to an attacker obtaining increased privileges on the device. The adversary, in this case, would need to carry out a man-in-the-middle attack to exploit this vulnerability.

There is also an access violation vulnerability — TALOS-2023-1696 (CVE-2023-23571) — that could lead to a denial-of-service if the attacker sends the device a specially crafted network request. TALOS-2023-1695 (CVE-2023-23547) can also be exploited with a specially crafted network request, but in this case, can lead to arbitrary file read.

Talos also discovered five vulnerabilities in the MilesightVPN:

Coverage

The following Snort rules will detect exploitation attempts against these vulnerabilities: 61206 – 61208, 61212, 61255 - 61258, 61266 - 61269, 61395 - 61397. Additional rules may be released in the future and current rules are subject to change, pending additional vulnerability information. For the most current rule information, please refer to your Cisco Secure Firewall or Snort.org.

Article Link: Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chain