九零登录

[【通过】] PHPcms9.6.0 最新版任意文件上传漏洞分析

[复制链接]
发表于 2017-4-13 15:26:38 | 显示全部楼层 |阅读模式

#####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

评分

参与人数 1酒票 +5 收起 理由
管理05 + 5 欢迎加入90!

查看全部评分

九零

GMT+8, 2026-10-22 14:32

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表