Phpcms v9漏洞分析
最近研究源碼審計(jì)相關(guān)知識(shí),會(huì)抓起以前開(kāi)源的CMS漏洞進(jìn)行研究,昨天偶然看見(jiàn)了這個(gè)PHPCMS的漏洞,就準(zhǔn)備分析研究一番,最開(kāi)始本來(lái)想直接從源頭對(duì)代碼進(jìn)行靜態(tài)分析,但是發(fā)現(xiàn)本身對(duì)PHPCMS架構(gòu)不是很熟悉,導(dǎo)致很難定位代碼的位置,***就采用動(dòng)態(tài)調(diào)試&靜態(tài)分析的方式對(duì)漏洞的觸發(fā)進(jìn)行分析,下面進(jìn)入正題。
1. 漏洞觸發(fā)代碼定位
通過(guò)漏洞的POC(/phpcms/index.php?m=member&c=index&a=register&siteid=1 )判斷,漏洞觸發(fā)點(diǎn)的入口位于/phpcms/modules/member/index.php文件中的register()方法中,在代碼中插入一些echo函數(shù),觀察輸出(見(jiàn)下)的變化。從下面的結(jié)果變化可知,img標(biāo)簽的src屬性是在執(zhí)行完下面的get()函數(shù):
- $user_model_info = $member_input->get($_POST['info'])
后發(fā)生變化,因此基本可以確定,漏洞的觸發(fā)點(diǎn)就是位于這個(gè)函數(shù)中。
2. 定位member_input->get()跟進(jìn)分析
跟進(jìn)該函數(shù),該函數(shù)位于/phpcms/modules/member/fields/member_input.class.php文件中,此處本來(lái)還想故技重施,在該方法中對(duì)代碼進(jìn)行插樁,但是發(fā)現(xiàn)插樁后的居然無(wú)法打印到頁(yè)面上,沒(méi)轍(原因望各位大神指點(diǎn)一二),只能對(duì)代碼進(jìn)行一行行推敲,先把代碼貼上,方便分析:
- 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;
- }
代碼整體比較容易,可能比較難理解的就是$this->fields這個(gè)參數(shù),這個(gè)參數(shù)是初始化類(lèi)member_input是插入的,這個(gè)參數(shù)分析起來(lái)比較繁瑣,主要是對(duì)PHPCMS架構(gòu)不熟,那就在此走點(diǎn)捷徑吧,在1中,直接將初始化完成后的member_input類(lèi)dump出來(lái),效果還不錯(cuò),所有的參數(shù)都dump到頁(yè)面上了,下面主要摘取比較重要的$this->fields[$field],即:【$this->fields["content"]】這個(gè)參數(shù),如下所示⤵:
- ["content"]=>
- array(35) {
- ["fieldid"]=>
- string(2) "90"
- ["modelid"]=>
- string(2) "11"
- ["siteid"]=>
- string(1) "1"
- ["field"]=>
- string(7) "content"
- ["name"]=>
- string(6) "內(nèi)容"
- ["tips"]=>
- string(407) "<div class="content_attr"><label><input name="add_introduce" type="checkbox" value="1" checked>是否截取內(nèi)容</label><input type="text" name="introcude_length" value="200" size="3">字符至內(nèi)容摘要
- <label><input type='checkbox' name='auto_thumb' value="1" checked>是否獲取內(nèi)容第</label><input type="text" name="auto_thumb_no" value="1" size="2" class="">張圖片作為標(biāo)題圖片
- </div>"
- ["css"]=>
- string(0) ""
- ["minlength"]=>
- string(1) "0"
- ["maxlength"]=>
- string(6) "999999"
- ["pattern"]=>
- string(0) ""
- ["errortips"]=>
- string(18) "內(nèi)容不能為空"
- ["formtype"]=>
- string(6) "editor"
- ["setting"]=>
- string(199) "array (
- 'toolbar' => 'full',
- 'defaultvalue' => '',
- 'enablekeylink' => '1',
- 'replacenum' => '2',
- 'link_mode' => '0',
- 'enablesaveimage' => '1',
- 'height' => '',
- 'disabled_page' => '0',
- )"
- ["formattribute"]=>
- string(0) ""
- ["unsetgroupids"]=>
- string(0) ""
- ["unsetroleids"]=>
- string(0) ""
- ["iscore"]=>
- string(1) "0"
- ["issystem"]=>
- string(1) "0"
- ["isunique"]=>
- string(1) "0"
- ["isbase"]=>
- string(1) "1"
- ["issearch"]=>
- string(1) "0"
- ["isadd"]=>
- string(1) "1"
- ["isfulltext"]=>
- string(1) "1"
- ["isposition"]=>
- string(1) "0"
- ["listorder"]=>
- string(2) "13"
- ["disabled"]=>
- string(1) "0"
- ["isomnipotent"]=>
- string(1) "0"
- ["toolbar"]=>
- string(4) "full"
- ["defaultvalue"]=>
- string(0) ""
- ["enablekeylink"]=>
- string(1) "1"
- ["replacenum"]=>
- string(1) "2"
- ["link_mode"]=>
- string(1) "0"
- ["enablesaveimage"]=>
- string(1) "1"
- ["height"]=>
- string(0) ""
- ["disabled_page"]=>
- string(1) "0"
- }
有了上面的參數(shù)列表后,理解get()函數(shù)的代碼就要輕松許多了,分析過(guò)程略。結(jié)論就是,漏洞的觸發(fā)函數(shù)在倒數(shù)6、7兩行,單獨(dú)截個(gè)圖,如下⤵:
這里比較重要的是要找出$func這個(gè)函數(shù),查查上面的表,找到["formtype"]=>string(6) “editor”,可知$func就是editor()函數(shù),editor函數(shù)傳入的參數(shù)就是上面列出的一長(zhǎng)串字符串,和img標(biāo)簽的內(nèi)容,下面將跟進(jìn)editor函數(shù),真相好像馬上就要大白于天下了。
3. 跟進(jìn)editor函數(shù)及后續(xù)函數(shù)
editor()函數(shù)位于/phpcms/modules/member/fields/editor/imput.inc.php文件中,老規(guī)矩,先貼出代碼:
- function editor($field, $value) {
- $setting = string2array($this->fields[$field]['setting']);
- $enablesaveimage = $setting['enablesaveimage'];
- if(isset($_POST['spider_img'])) $enablesaveimage = 0;
- if($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;
- }
簡(jiǎn)單閱讀代碼,發(fā)現(xiàn)實(shí)際的觸發(fā)流程發(fā)生在$this->attachment->download()函數(shù)中,直接跟進(jìn)這個(gè)函數(shù),這個(gè)函數(shù)位于/phpcms/libs/classes/attachment.class.php中download()函數(shù),源代碼有點(diǎn)長(zhǎng),就貼一些關(guān)鍵代碼,⤵
- $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)) {
代碼主要是進(jìn)行一些正則過(guò)濾等等操作,這里真正關(guān)鍵的是代碼的***一行的操作$upload_func($file, $newfile),其中$this->upload_func = ‘copy’;,寫(xiě)的在明白點(diǎn)就是copy($file, $newfile),漏洞就是一個(gè)copy操作造成的。