~# n0tr00t Security Team

然之协同及喧喧聊天系统远程命令执行漏洞

27 Feb 2018 - niexinming

[+] Author: niexinming
[+] Team: n0tr00t security team
[+] From: http://www.n0tr00t.com
[+] Create: 2018-02-27

关于漏洞

这个漏洞比较有趣,写出来给大家分享一下,漏洞影响的版本有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函数去操纵数据库,添加一个管理员账号,因为然之后台可以查看网站的绝对地址:

image

数据库密码:

image

执行任意命令:

image

关于 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 提醒厂商修复漏洞,但是厂商开发人员认为影响不大,直到现在厂商未修复此漏洞

临时防护建议

在然之后台进入-》后台管理-》喧喧,将密钥改成任意值。