写在前面

上网中无意碰到一个cms,故从官网上拉了源码,本地看了一个下午,发现个上传,cnvd没有相关记录,就提交了。

利用

后台的上传,实际没太大作用,全当锻炼一下看代码能力。。。。。先说怎么利用吧:

后台存在\application\admin\controller\Weapp.php 控制器

但是后台管理界面未找到对应功能点,于是通过抓包访问:

修改标红部分:

到达插件上传界面:

然后上传插件weapp.zip(文件名就叫weapp)
其内部格式如下:

其中.htaccess 文件内容为空;
ssss.php 为shell 文件;
config.php 的文件内容需要和\data\weapp\Sample\weapp\Sample\config.php 中的内容格式保持一致,其中键不变,值可以修改;
例如:

然后上传,上传后会显示插件不存在,但是网站根目录下的weapp文件中已经保存了上传的文件:

访问ssss.php getshell

分析

总体来说不难,就是绕来绕去。。。
首先这个插件上传功能点就找了很久没找到,因为主要还是黑盒为主测试,后来还是看源码的发现有这么个东西。
路由什么的就不说了,基本上就那样,admin下面Weapp.php中存在upload方法:

这个上传并解压看上去就很有搞头

public function upload() 
    {
        //防止php超时
        function_exists('set_time_limit') && set_time_limit(0);

        if (IS_AJAX_POST) {

            $admin_info = session('admin_info');

            /*只限于创始人安装*/
            if (empty($admin_info) || -1 != $admin_info['role_id']) {
                $this->error('没有安装权限!');
            }
            /*--end*/

            if (empty($admin_info['weapp_info']['firstInstallpwd'])) {
                $pwd = input('post.pwd/s');
                $installpwd = func_encrypt($pwd);
                if (empty($installpwd)) {
                    $this->error('请录入插件安装密码!');
                } else {
                    $weapp_installpwd = tpCache('weapp.weapp_installpwd');
                    if ($weapp_installpwd != $installpwd) {
                        $this->error('插件安装密码不正确!');
                    }
                }
                $admin_info['weapp_info']['firstInstallpwd'] = $installpwd;
                session('admin_info', $admin_info);
            }

            $fileExt = 'zip';
            $savePath = UPLOAD_PATH.'tmp'.DS;
            $image_upload_limit_size = intval(tpCache('basic.file_size') * 1024 * 1024);
            $file = request()->file('weappfile');
            if(empty($file)){
                $this->error('请先上传zip文件');
            }
            $error = $file->getError();
            if(!empty($error)){
                $this->error($error);
            }
            $result = $this->validate(
                ['file' => $file], 
                ['file'=>'fileSize:'.$image_upload_limit_size.'|fileExt:'.$fileExt],
                ['file.fileSize' => '上传文件过大','file.fileExt'=>'上传文件后缀名必须为'.$fileExt]
            );
            if (true !== $result || empty($file)) {
                $this->error($result);
            }
            // 移动到框架应用根目录/public/upload/tmp/ 目录下
            $folderName = session('admin_id').'-'.dd2char(date("ymdHis").mt_rand(100,999));  // 文件名,不带扩展名
            $fileName = $folderName.'.'.$fileExt; // 上传之后的文件全名
            /*使用自定义的文件保存规则*/
            $info = $file->rule(function ($file) {
                return  $folderName;
            })->move($savePath, $folderName);
            /*--end*/
            if ($info) {
                $filepath = $savePath.$fileName;
                if (file_exists($filepath)) {
                    /*解压之前,删除存在的文件夹*/
                    delFile($savePath.$folderName);
                    /*--end*/

                    /*解压文件*/
                    $zip = new \ZipArchive();//新建一个ZipArchive的对象
                    if ($zip->open($savePath.$fileName) != true) {
                        $this->error("插件压缩包读取失败!", url('Weapp/index'));
                    }
                    $zip->extractTo($savePath.$folderName.DS);//假设解压缩到在当前路径下插件名称文件夹内
                    $zip->close();//关闭处理的zip文件
                    /*--end*/

                    /*获取插件目录名称*/
                    $dirList = glob($savePath.$folderName.DS.WEAPP_DIR_NAME.DS.'*');
                    $weappPath = !empty($dirList) ? $dirList[0] : '';
                    if (empty($weappPath)) {
                        @unlink(realpath($savePath.$fileName));
                        delFile($savePath.$folderName, true);
                        $this->error('插件压缩包缺少目录文件', url('Weapp/index'));
                    }

                    $weappPath = str_replace("\\", DS, $weappPath);
                    $weappPathArr = explode(DS, $weappPath);
                    $weappName = $weappPathArr[count($weappPathArr) - 1];
                    // if (is_dir(ROOT_PATH.WEAPP_DIR_NAME.DS.$weappName)) {
                    //     $this->error("已存在同名插件{$weappName},请手工移除".WEAPP_DIR_NAME.DS.$weappName."目录");
                    // }
                    /*--end*/

                    /*修复非法插件上传,导致任意文件上传的漏洞*/
                    $configfile = $savePath.$folderName.DS.WEAPP_DIR_NAME.DS.$weappName.'/config.php';
                    if (!file_exists($configfile)) {
                        @unlink(realpath($savePath.$fileName));
                        delFile($savePath.$folderName, true);
                        $this->error('插件不符合标准!', url('Weapp/index'));
                    } else {
                        $configdata = include($configfile);
                        if (empty($configdata) || !is_array($configdata)) {
                            @unlink(realpath($savePath.$fileName));
                            delFile($savePath.$folderName, true);
                            $this->error('插件不符合标准!', url('Weapp/index'));
                        } else {
                            $sampleConfig = include(DATA_NAME.DS.'weapp'.DS.'Sample'.DS.'weapp'.DS.'Sample'.DS.'config.php');
                            foreach ($configdata as $key => $val) {
                                if ('permission' != $key && !isset($sampleConfig[$key])) {
                                    @unlink(realpath($savePath.$fileName));
                                    delFile($savePath.$folderName, true);
                                    $this->error('插件不符合标准!', url('Weapp/index'));
                                }
                            }
                        }
                    }
                    /*--end*/

                    // 递归复制文件夹            
                    $copy_bool = recurse_copy($savePath.$folderName, rtrim(ROOT_PATH, DS));
                    if (true !== $copy_bool) {
                        $this->error($copy_bool);
                    }

                    /*删除上传的插件包*/
                    @unlink(realpath($savePath.$fileName));
                    @delFile($savePath.$folderName, true);
                    /*--end*/

                    /*安装插件*/
                    $configfile = WEAPP_DIR_NAME.DS.$weappName.'/config.php';
                    if (file_exists($configfile)) {
                        $configdata = include($configfile);
                        $code = isset($configdata['code']) ? $configdata['code'] : 'error_'.date('Ymd');
                        Db::name('weapp')->where(['code'=>$code])->delete();

                        $addData = [
                            'code'          => $code,
                            'name'          => isset($configdata['name']) ? $configdata['name'] : '配置信息不完善',
                            'config'        => empty($configdata) ? '' : json_encode($configdata),
                            'data'        => '',
                            'add_time'      => getTime(),
                        ];
                        $weapp_id = Db::name('weapp')->insertGetId($addData);
                        if (!empty($weapp_id)) {
                            $this->install($weapp_id);
                        }
                    }
                    /*--end*/
                }
            }else{
                //上传错误提示错误信息
                $this->error($info->getError());
            }
        }
    }

有注释,好看懂,给作者点赞。
下面这段主要意思大概是设置一个上传密码,然后第一次上传会进行验证,不是重点,带过带过。。。

function_exists('set_time_limit') && set_time_limit(0);

        if (IS_AJAX_POST) {

            $admin_info = session('admin_info');

            /*只限于创始人安装*/
            if (empty($admin_info) || -1 != $admin_info['role_id']) {
                $this->error('没有安装权限!');
            }
            /*--end*/

            if (empty($admin_info['weapp_info']['firstInstallpwd'])) {
                $pwd = input('post.pwd/s');
                $installpwd = func_encrypt($pwd);
                if (empty($installpwd)) {
                    $this->error('请录入插件安装密码!');
                } else {
                    $weapp_installpwd = tpCache('weapp.weapp_installpwd');
                    if ($weapp_installpwd != $installpwd) {
                        $this->error('插件安装密码不正确!');
                    }
                }
                $admin_info['weapp_info']['firstInstallpwd'] = $installpwd;
                session('admin_info', $admin_info);
            }

从681行开始看:

先设定了后缀(zip)、路径等信息,然后进行验证等,最后在/public/upload/tmp/下创建临时文件,调试进入if ($info)判断:

这里看到725行:

$dirList = glob($savePath.$folderName.DS.WEAPP_DIR_NAME.DS.'*');
                    $weappPath = !empty($dirList) ? $dirList[0] : '';
                    if (empty($weappPath)) {
                        @unlink(realpath($savePath.$fileName));
                        delFile($savePath.$folderName, true);
                        $this->error('插件压缩包缺少目录文件', url('Weapp/index'));
                    }

通过glob获取后面那个文件里面的信息,其中WEAPP_DIR_NAME是:

这个已经指定了为weapp,所以插件的压缩包要以这个命名,否则$dirList就为空了,进而$weappPath就为空,进入if (empty($weappPath))就会报错。
再往下看:

743行会验证是否存在这个config.php 文件,749行还要验证文件内容是否为数组,所以前面利用时,在weapp下新建AAAAAAA文件夹,文件夹里面就是内容特定的config.php文件,具体什么内容,下面再说,

接着看下面:

进入else之后,会include一个文件,内容是\data\weapp\Sample\weapp\Sample\config.php:

Include这个文件后会赋值给$sampleConfig变量,然后会将上传的config.php中的内容与$sampleConfig进行键值相关内容对比,因此前面说的config.php内容为数组,内容要与这个文件内容结构保持一致就是这个意思,就是防止进入if判断然后报错。

然后往下走进入recurse_copy:

跟进recurse_copy:

这里的$dst就是网站根目录,然后$file = readdir($dir)就是我们上传的压缩包的名称,
weapp.zip 对应的文件夹名称就是 weapp,因此通过该函数会将上传的插件内容复制到网站根目录下的weapp文件夹中。

而原本网站根目录下刚好存在weapp文件,其中有个.htaccess文件

该文件限制了php脚本的执行

因此上传的时候要传一个空的.htaccess文件来覆盖原本的文件,从而解除限制。
复制完成之后:

这时候访问ssss.php就能getshell了

后话

分析得有点稍显凌乱,上传那个插件后面都没跟了,有大佬有兴趣可以跟下,记得带带我,嘻嘻嘻

点击收藏 | 0 关注 | 1
登录 后跟帖