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参数中解析当前请求的模块、控制器和操作,下面是一个标准的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

参考链接:
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');

/**
 * 获取输入参数 支持过滤和默认值
 * 使用方法:
 * 
 * I('id',0); 获取id参数 自动判断get或者post
 * I('post.name','','htmlspecialchars'); 获取$_POST['name']
 * I('get.'); 获取$_GET
 * 
 * @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的文件内容

 

参考文章

https://xz.aliyun.com/t/8021