最新然之协同(包含专业版)及喧喧及时聊天系统远程命令执行漏洞详解

作者:niexinming @ n0tr00t security team

来源:http://www.n0tr00t.com

关于漏洞

这个漏洞比较有趣,写出来给大家分享一下

这个漏洞影响的版本有ranzhi协同oa<=4.6.1(包含专业版)还有喧喧及时聊天系统<=1.3

出问题的地方是喧喧聊天系统,由于然之开源版和专业版自4.0之后都自带这个聊天系统,所以都会被影响

从官网下周然之4.6.1之后首先看ranzhiwwwxuanxuan.php,这个文件是喧喧的入口,加载的模块在ranzhiframeworkxuanxuan.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里面的,也就是只能加载和调用ranzhiappsyschatcontrol.php这里面的函数,由于调用的函数名可以控制,其实可以调用继承的父类种函数,对,这个漏洞最关键一点是可以调用父类函数,看一下,这个chat类继承于control

class chat extends control

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

临时防护建议:

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