什么版本?
写在前面
上网中无意碰到一个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了
后话
分析得有点稍显凌乱,上传那个插件后面都没跟了,有大佬有兴趣可以跟下,记得带带我,嘻嘻嘻