#####PHPcms9.6.0 最新版任意文件上传漏洞分析
By:mapl0
漏洞的利用以及exp已经在freebuf和一些博客上有了:http://www.freebuf.com/vuls/131648.html
代码分析如下:
通过exp可看出入口是在phpcms/modules/member/index.php
在106行:
if($member_setting['enablemailcheck']) { //是否需要邮件验证
$userinfo['groupid'] = 7;
} elseif($member_setting['registerverify']) { //是否需要管理员审核
$modelinfo_str = $userinfo['modelinfo'] = isset($_POST['info']) ? array2string(array_map("safe_replace", new_html_special_chars($_POST['info']))) : '';
$this->verify_db = pc_base::load_model('member_verify_model');
unset($userinfo['lastdate'],$userinfo['connectid'],$userinfo['from']);
$userinfo['modelinfo'] = $modelinfo_str;
$this->verify_db->insert($userinfo);
showmessage(L('operation_success'), 'index.php?m=member&c=index&a=register&t=3');
} else {
//查看当前模型是否开启了短信验证功能
$model_field_cache = getcache('model_field_'.$userinfo['modelid'],'model');
if(isset($model_field_cache['mobile']) && $model_field_cache['mobile']['disabled']==0) {
$mobile = $_POST['info']['mobile'];
if(!preg_match('/^1([0-9]{10})$/',$mobile)) showmessage(L('input_right_mobile'));
$sms_report_db = pc_base::load_model('sms_report_model');
$posttime = SYS_TIME-300;
$where = "`mobile`='$mobile' AND `posttime`>'$posttime'";
$r = $sms_report_db->get_one($where);
if(!$r || $r['id_code']!=$_POST['mobile_verify']) showmessage(L('error_sms_code'));
}
$userinfo['groupid'] = $this->_get_usergroup_bypoint($userinfo['point']);
}
//附表信息验证 通过模型获取会员信息
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']);
}
重点在这一行:
$user_model_info = $member_input->get($_POST[‘info']);
我们跟进caches/caches_model/caches_data/member_input_class.php
第20行:
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) {
if($data['islink']==1 && !in_array($field,$debar_filed)) continue;
$field = safe_replace($field);
$name = $this->fields[$field]['name'];
$minlength = $this->fields[$field]['minlength'];
$maxlength = $this->fields[$field]['maxlength'];
$pattern = $this->fields[$field]['pattern'];
$errortips = $this->fields[$field]['errortips'];
if(empty($errortips)) $errortips = "$name 不符合要求!";
$length = empty($value) ? 0 : strlen($value);
if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!");
if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');
if($maxlength && $length > $maxlength && !$isimport) {
showmessage("$name 不得超过 $maxlength 个字符!");
} else {
str_cut($value, $maxlength);
}
if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips);
if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!");
$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);
$info[$field] = $value;
}
}
return $info;
在if(method_exists($this, $func)) $value = $this->$func($field, $value);
其中,$field为exp里info[content]中的content,$value为info[content]的值;
然后跳转到editor方法
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;
}
再跟踪一下,到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;
其中$string是我们传入的info[content],$ext为gif|jpg|jpeg|bmp|png,这里需要匹配成功,原意是通过ext限制后缀为图片,但把控不严格,先往下接着看代码。
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]=='/') {
这里有一步很关键 $pos = strpos($surl,'#');
if($pos>0) $surl = substr($surl,0,$pos);
匹配出来#然后去掉#后的内容,那么问题就来了,之前的正则控制了我们上传文件的后缀,但走到这一步的时候,如果有#就会去除#后内容,那如果写成.php#.png这样的,经过substr后,后缀又变成了php。
继续往下走:
在166行:
$filename = fileext($file);
$file_name = basename($file);
$filename = $this->getname($filename);
这里$filename为我们可控的后缀,跟入getname;
function getname($fileext){
return date('Ymdhis').rand(100, 999).'.'.$fileext;
}
返回的文件就是随机数.php
就此getshell。
总的来说,漏洞的关键点就在于substr($surl,0,$pos);这里让之前的正则过滤变成了无用功,导致我们可以用上传附件的方法上传任意后缀文件,漏洞修复方法应该注重于substr和正则的逻辑矛盾。
Ps:审计菜鸡的小尝试,如果有错误的地方,欢迎各路大佬指出:-D
|