作者:niexinming @ n0tr00t security team
來源:http://www.n0tr00t.com

關于漏洞

這個漏洞比較有趣,寫出來給大家分享一下

這個漏洞影響的版本有ranzhi協同oa<=4.6.1(包含專業版)還有喧喧及時聊天系統<=1.3

出問題的地方是喧喧聊天系統,由于然之開源版和專業版自4.0之后都自帶這個聊天系統,所以都會被影響

從官網下周然之4.6.1之后首先看ranzhi\www\xuanxuan.php,這個文件是喧喧的入口,加載的模塊在ranzhi\framework\xuanxuan.class.php,由于聊天信息是用aes加密過的,初始的密鑰是88888888888888888888888888888888,我相信沒有幾個人會去改的吧,所以漏洞一開始就已經埋下來了

再往下看,看到118行的parseRequest這個函數,看看這個系統是怎么處理傳遞進來的參數的

    public function parseRequest()
    {
        $input   = file_get_contents("php://input");
        $input   = $this->decrypt($input);
        $userID  = !empty($input->userID) ? $input->userID : '';
        $module  = !empty($input->module) ? $input->module : '';
        $method  = !empty($input->method) ? $input->method : '';
        $params  = !empty($input->params) ? $input->params : array();

        if(!$module or !$method or $module != 'chat')
        {
            $data = new stdclass();
            $data->module = 'chat';
            $data->method = 'kickoff';
            $data->data   = 'Illegal Requset.';
            die($this->encrypt($data));
        }

        if($module == 'chat' && $method == 'login' && is_array($params))
        {
            /* params[0] is the server name. */
            unset($params[0]);
        }
        if($userID && is_array($params))
        {
            $params[] = $userID;
        }

        $this->setModuleName($module);
        $this->setMethodName($method);
        $this->setParams($params);
        $this->setControlFile();
    }

首先,從原始post數據獲取數據,解密,獲取userID,module,method,params這幾個參數,其中userID的用戶id,module是調用模塊,method是調用的方法,params是傳遞的參數,這里有一個限制,模塊只能加載chat里面的,也就是只能加載和調用ranzhi\app\sys\chat\control.php這里面的函數,由于調用的函數名可以控制,其實可以調用繼承的父類種函數,對,這個漏洞最關鍵一點是可以調用父類函數,看一下,這個chat類繼承于control

class chat extends control

control類在ranzhi\framework\control.class.php,可以看到這個類里面只有一個函數就是fetch函數,但是這個類又繼承了baseControl這個類,但是已經不重要了,用這個函數就可以了

這個函數在前面檢查模塊是否存在之后就把參數放入call_user_func_array中了

    public function fetch($moduleName = '', $methodName = '', $params = array(), $appName = '')
    {
        if($moduleName == '') $moduleName = $this->moduleName;
        if($methodName == '') $methodName = $this->methodName;
        if($appName == '')    $appName    = $this->appName;
        if($moduleName == $this->moduleName and $methodName == $this->methodName) 
        {
            $this->parse($moduleName, $methodName);
            return $this->output;
        }

        $currentPWD = getcwd();

        /**
         * 設置引用的文件和路徑。
         * Set the pathes and files to included.
         **/
        $modulePath        = $this->app->getModulePath($appName, $moduleName);
        $moduleControlFile = $modulePath . 'control.php';
        $actionExtPath     = $this->app->getModuleExtPath($appName, $moduleName, 'control');
        $file2Included     = $moduleControlFile;

        if(!empty($actionExtPath))
        {
            $commonActionExtFile = $actionExtPath['common'] . strtolower($methodName) . '.php';
            $file2Included       = file_exists($commonActionExtFile) ? $commonActionExtFile : $moduleControlFile;

            if(!empty($actionExtPath['site']))
            {
                $siteActionExtFile = $actionExtPath['site'] . strtolower($methodName) . '.php';
                $file2Included     = file_exists($siteActionExtFile) ? $siteActionExtFile : $file2Included;
            }
        }

        /**
         * 加載控制器文件。
         * Load the control file. 
         */
        if(!is_file($file2Included)) $this->app->triggerError("The control file $file2Included not found", __FILE__, __LINE__, $exit = true);
        chdir(dirname($file2Included));
        if($moduleName != $this->moduleName) helper::import($file2Included);

        /**
         * 設置調用的類名。
         * Set the name of the class to be called. 
         */
        $className = class_exists("my$moduleName") ? "my$moduleName" : $moduleName;
        if(!class_exists($className)) $this->app->triggerError(" The class $className not found", __FILE__, __LINE__, $exit = true);

        /**
         * 解析參數,創建模塊control對象。
         * Parse the params, create the $module control object. 
         */
        if(!is_array($params)) parse_str($params, $params);
        $module = new $className($moduleName, $methodName, $appName);

        /**
         * 調用對應方法,使用ob方法獲取輸出內容。
         * Call the method and use ob function to get the output. 
         */
        ob_start();
        call_user_func_array(array($module, $methodName), $params);
        $output = ob_get_contents();
        ob_end_clean();

        /**
         * 返回內容。
         * Return the content. 
         */
        unset($module);

        chdir($currentPWD);
        return $output;
    }
}

call_user_func_array(array($module, $methodName), $params);這個函數的調用相當于$module::$methodName($params),$methodName只能是public類型才可以,可以利用call_user_func_array調用php的任意內置類的public函數,也可以調用include的任意類,所以我在不斷嘗試之后,最終選擇調用baseDAO類的query函數去操縱數據庫,添加一個管理員賬號,因為然之后臺可以查看網站的絕對地址:

數據庫密碼:

執行任意命令:

關于poc的構造:

【1】首先是exp函數,因為數據傳輸是依靠aes加密傳輸的,而初始化的aes密鑰是88888888888888888888888888888888,所以把exp的json數據加密post給服務器端就好

而exp函數中的這個data就是整個exp的關鍵部分

data = '{"userID": "123","module": "chat","method": "fetch","params": {"0":"baseDAO","1":"query","2":"'+sql+'","3":"sys"}}'

【2】module是調用的模塊名字,因為受到限制,所以只能調用chat模塊,而method是調用的方法名字,因為這個沒有限制,所以就可以調用父類的函數fetch,傳遞進去的params又可以繼續調用其它模塊的其他函數,但是只能調用php中內置類的public函數,和include中的public函數,所以公共模塊是一個很好的利用點,而公共模塊中的數據庫操作函數最好下手,所以就調用baseDAO中query這個函數,往里面傳遞sql語句就可以控制數據庫了

【3】在數據庫中插入一個管理員的賬號之后就可以登陸后臺為所欲為了

然之登陸有點意思登陸函數是Login_ranzhi,ranzhi登陸前要先發get請求給登陸頁面,讓cookie獲取rid和頁面中獲取v.random。在登陸時要向登陸頁面發送賬號:account,密碼: password,密碼是由MD5(MD5(MD5(明文密碼)+賬號)+v.random)生成,原始密碼:rawPassword,由MD5(明文密碼)生成,keepLogin:false

【4】登陸后就可以在后臺獲得網站絕對路徑和數據庫帳戶名和密碼,也可以利用后臺執行任意命令

【5】因為然之演示站限制了很多函數的執行,所以利用exp添加管理員是可以做到的,后面執行系統命令被限制就無法去實現

漏洞披露

  1. 2018-01-08 給cnnvd提交漏洞
  2. 2018-01-09 給360補天提交漏洞
  3. 2018-01-11 cnnvd回復郵件確定漏洞真實存在
  4. 2018-01-12 360補天定為通用型漏洞
  5. 2018-2-24 提醒廠商修復漏洞,但是廠商開發人員認為影響不大,直到現在廠商未修復此漏洞

臨時防護建議:

在然之后臺進入-》后臺管理-》喧喧,把密鑰改成任意值


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/534/