86

0x00 CMS核心目录

网站使用框架:Thinkphp
App 应用目录
data 网站上传配置、编辑器目录
include 网站配置调式目录
install 安装目录
public 公共目录
uploads 上传目录

环境:

CMS:XYHCMS V3.5

PHP:5.6

MYSQL:5.7.26

0x01 后台任意文件上传

漏洞利用

登录后台,有一个可修改网站配置的选项

image-20200825141459656

当在允许附件类型或是允许图片类型处添加php后,可直接上传webshell获取权限。

image-20200825142048017

image-20200825142334095

image-20200825142606065

漏洞分析

网站设置——>App/controller/SystemController.class.php/194行 site函数

public function site() {
if (IS_POST) {

$data = I('config', array(), 'trim');//获取网站配置信息
//遍历网站配置项是否存在<?php字符串,如果存在字符串使用空字符串替换掉
foreach ($data as $key => $val) {
if (stripos($val, '<?php') !== false) {
//问题点,由于此处匹配的是<?php,未匹配php,所以在允许上传的后缀中可以添加php。
$data[$key] = preg_replace('/<\?php(.+?)\?>/i', '', $val);
}
}

$data['CFG_IMGTHUMB_SIZE'] = strtoupper($data['CFG_IMGTHUMB_SIZE']);//设置全大写
$data['CFG_IMGTHUMB_SIZE'] = str_replace(array(',', 'X'), array(',', 'X'), $data['CFG_IMGTHUMB_SIZE']);//将中文逗号替换为英文逗号
if (empty($data['CFG_IMGTHUMB_SIZE'])) {
$this->error('缩略图组尺寸不能为空');
}

if (!empty($data['CFG_IMAGE_WATER_FILE'])) {
$img_ext = pathinfo($data['CFG_IMAGE_WATER_FILE'], PATHINFO_EXTENSION);//获取路径信息
$img_ext = strtolower($img_ext);
if (!in_array($img_ext, array('jpg', 'gif', 'png', 'jpeg'))) {//此处判断文件类型,如果不是则提示用户重新上传
$this->error('水印图片文件不是图片格式!请重新上传!');
return;
}

}

//
foreach ($data as $k => $v) {
$ret = M('config')->where(array('name' => $k))->save(array('s_value' => $v));
}
if ($ret !== false) {
F('config/site', null);
$this->success('修改成功', U('System/site'));

} else {

$this->error('修改失败!');
}

exit();
}

添加水印图片——> App/Manage/Controller/PublicController.class.php/67行 upload函数

public function upload($img_flag = 0) {
header("Content-Type:text/html; charset=utf-8"); //不然返回中文乱码
$result = array('state' => '失败', 'url' => '', 'name' => '', 'original' => '');
$sub_path = I('post.sfile', '', 'trim,htmlspecialchars'); //判断其他子目录

$img_flag = empty($img_flag) ? 0 : 1;


$yun_upload = new \Common\Lib\YunUpload($img_flag, $sub_path);
$upload_result = $yun_upload->upload();//此处调用了上传功能,根据此函数

if ($upload_result['status']) {
$result['state'] = 'SUCCESS';
$result['info'] = $upload_result['data'];
} else {
$result['state'] = $upload_result['info'];
}
echo json_encode($result);

}

App/Common/lib/YunUpload.class.php/ 62行 upload函数

public function upload() {
$result = array('status' => 0, 'info' => '', 'data' => array());
//文件上传地址提交给他,并且上传完成之后返回一个信息,让其写入数据库
if (empty($_FILES)) {
$result['info'] = '必须选择上传文件';//如果未获取到参数,提示用户上传
return $result;
} else {

$info = $this->_upload(); //获取图片信息

if (isset($info) && is_array($info)) {
//写入数据库的自定义c方法
if (!$this->_saveDate($info)) {
//echo '上传入库失败';
$result['status'] = 0;
$result['info'] = '上传入库失败';
return $result;
}

//数组索引转为数字
$new_info = array();
foreach ($info as $k => $v) {

$v['url'] = get_url_path(C('CFG_UPLOAD_ROOTPATH')) . $v['savepath'] . $v['savename'];
//是否有缩略图
if ($this->thumFlag) {
//读取缩略图配置信息
$imgtbSize = C('CFG_IMGTHUMB_SIZE'); //配置缩略图第一个参数
$imgTSize = explode('X', $imgtbSize[0]);
if (!empty($imgTSize)) {
$v['turl'] = get_picture($v['url'], $imgTSize[0], $imgTSize[1]);
}
}
$new_info[] = $v;
}

$result['status'] = 1;
$result['info'] = '上传成功';
$result['data'] = $new_info;

return $result;

} else {
$result['info'] = '失败:' . $info;
return $result;
}
}

}

App/Common/lib/YunUpload.class.php/ 231行 _upload函数 对比上传后缀名是否在网站配置中

public function _upload() {
$ext = ''; //原文件后缀
foreach ($_FILES as $key => $v) {//获取文件后缀名
$strtemp = explode('.', $v['name']);
$ext = end($strtemp); //获取文件后缀,或$ext = end(explode('.', $_FILES['fileupload']['name']));
break;
}

$upload = new \Think\Upload(); //new Upload($config)
//修配置项
$upload->autoSub = true; //是否使用子目录保存图片
$upload->subType = 'date'; //子目录保存规则
$upload->subName = array('date', 'Ymd');
$upload->maxSize = get_upload_maxsize(); //设置上传文件大小
$upload->exts = $this->allowType; //设置上传文件类型
$upload->rootPath = $this->rootPath; //上传根路径
$upload->savePath = $this->subDirectory; //上传(子)目录
$upload->saveName = array('uniqid', ''); //上传文件命名规则
$upload->replace = true; //存在同名是否覆盖
$upload->callback = false; //检测文件是否存在回调函数,如果存在返回文件信息数组

if ($info = $upload->upload()) {

//判断是添加水印--有缩略的才添加水印
if ($this->thumFlag) {
$this->_doWater($info);
}

//是否有缩略图
if ($this->thumFlag) {
$this->_doThum($info);
}
return $info;

} else {

//$str = array('err' =>1 ,'msg' => $upload->getError() );
return $upload->getError();
}

}

当程序执行到75行时,程序new了 yunupload类,并调用了上传功能。

image-20200825165551625

跳转到YunUpload类中的upload函数,判断是否获取到图片信息,然后调用_upload函数对比图片信息是否与配置文件中一致

image-20200825170724575

遍历获取到上传文件的文件名以及后缀名,程序会将后缀名和allowType数组进行对比,发现后缀名是允许类型,程序会正常执行

image-20200825171239967

当YunUpload类中的upload函数执行完成后,返回上传成功后的文件地址。

image-20200825172030067

程序返回Public类的upload函数执行最后流程,结束后,添加水印图片整个流程结束,webshell上传成功。

image-20200825172340988

回到网站设置App/controller/SystemController.class.php site函数

当执行到遍历代码时,程序会将网站配置所有项全部遍历查找是否存在<?php字符串

image-20200825172945671

当执行到215行时,此处做了一个水印图片后缀过滤,网站获取到的是php后缀,所以此处对比不成功,网站报错提示

image-20200825173222671

但是由于200行处正则处未正确匹配字符串,导致可上传文件,webshell已上传成功,所以此处无法保存不影响漏洞点。

0x02 后台代码执行

漏洞利用

Payload:
<? phpinfo(); ?>
<?=phpinfo();?>

image-20200825175045437

image-20200825175101573

image-20200825175109704

漏洞分析

网站设置——>App/controller/SystemController.class.php/194行 site函数

public function site() {
if (IS_POST) {

$data = I('config', array(), 'trim');//获取网站配置信息
//遍历网站配置项是否存在<?php字符串,如果存在字符串使用空字符串替换掉
foreach ($data as $key => $val) {
if (stripos($val, '<?php') !== false) {
//问题点,由于此处匹配的是<?php,使用短标签或其他风格写入php代码,访问生成的site文件即可
$data[$key] = preg_replace('/<\?php(.+?)\?>/i', '', $val);
}
}

$data['CFG_IMGTHUMB_SIZE'] = strtoupper($data['CFG_IMGTHUMB_SIZE']);//设置全大写
$data['CFG_IMGTHUMB_SIZE'] = str_replace(array(',', 'X'), array(',', 'X'), $data['CFG_IMGTHUMB_SIZE']);//将中文逗号替换为英文逗号
if (empty($data['CFG_IMGTHUMB_SIZE'])) {
$this->error('缩略图组尺寸不能为空');
}

if (!empty($data['CFG_IMAGE_WATER_FILE'])) {
$img_ext = pathinfo($data['CFG_IMAGE_WATER_FILE'], PATHINFO_EXTENSION);//获取路径信息
$img_ext = strtolower($img_ext);
if (!in_array($img_ext, array('jpg', 'gif', 'png', 'jpeg'))) {//此处判断文件类型,如果不是则提示用户重新上传
$this->error('水印图片文件不是图片格式!请重新上传!');
return;
}

}

//
foreach ($data as $k => $v) {
$ret = M('config')->where(array('name' => $k))->save(array('s_value' => $v));
}
if ($ret !== false) {
F('config/site', null);
$this->success('修改成功', U('System/site'));

} else {

$this->error('修改失败!');
}

exit();
}

遍历匹配是否存在绕过

image-20200825180228538

遍历完成后,执行到209行后,跳到222行执行,此处会将所有的参数及参数值遍历后存入数据库并且写入config/site文件中保存

image-20200825180617642

而site文件后缀是php,导致代码执行。

0x03 后台任意文件删除

漏洞利用

漏洞点在删除数据库文件备份,抓包

image-20200826160916538

由于未做目录限制,导致可跨目录删除任意文件。可删除install.lock文件来重置网站程序。

image-20200826161740066

漏洞分析

数据库备份->App/manage/controller/databasecontroller.class.php/ 293行 delSqlFiles函数

public function delSqlFiles() {

$id = I('id', 0, 'intval');//过滤ID参数
$batchFlag = I('get.batchFlag', 0, 'intval');//过滤GET传参
//批量删除
if ($batchFlag) {
$files = I('key', array());
} else {
$files[] = I('sqlfilename', '');//删除单个
}

if (empty($files)) {
$this->error('请选择要删除的sql文件');
}

foreach ($files as $file) {//遍历删除
//此处将需要删除的文件直接带入到了unlink函数,且未做危险字符过滤,导致用户可跨目录删除文件
unlink($this->getDbPath() . '/' . $file);
}
$this->success("已删除:" . implode(",", $files), U('Database/restore'));

}

image-20200826164041349

最后成功删除license.txt