开源项目CRMEB 任意文件下载漏洞分析
项目地址:https://github.com/crmeb/CRMEB
下载源码后来到app/adminapi/controller/v1/marketing/live/LiveGoods.php
文件add
函数。
源码如下:
public function add() { [$goods_info] = $this->request->postMore([ ['goods_info', []] ], true); if (!$goods_info) return app('json')->fail('请选择商品'); foreach ($goods_info as $goods) { if (!$goods['id']) return app('json')->fail('请选择商品'); if (!$goods['store_name']) return app('json')->fail('请输入名称'); if (!$goods['image']) return app('json')->fail('请选择背景图'); if (!$goods['price']) return app('json')->fail('请输入直播价格'); if ($goods['price'] <= 0) return app('json')->fail('直播价格必须大于0'); } $this->services->add($goods_info); return app('json')->success('添加成功'); }
函数从前端接受一个goods_info
参数赋值于变量$goods_info
[$goods_info] = $this->request->postMore([ ['goods_info', []] ], true);
通过跟踪$goods_info
参数进入$services
对象的add
函数
$this->services->add($goods_info);
在本类中可以看到services
声明为类LiveGoodsServices
public function __construct(App $app, LiveGoodsServices $services) { parent::__construct($app); $this->services = $services; }
来到文件app/services/activity/live/LiveGoodsServices.php
,函数add
源码如下:
public function add(array $goods_info) { $product_ids = array_column($goods_info, 'id'); $this->create($product_ids); $miniUpload = MiniProgramService::materialTemporaryService(); $download = app()->make(DownloadImageService::class); $dataAll = $data = []; $time = time(); foreach ($goods_info as $product) { $data = [ 'product_id' => $product['id'], 'name' => Str::substrUTf8($product['store_name'], 12, 'UTF-8', ''), 'cover_img' => $product['image'] ?? '', 'price_type' => 1, 'cost_price' => $product['cost_price'] ?? 0.00, 'price' => $product['price'] ?? 0.00, 'url' => 'pages/goods_details/index?id=' . $product['id'], 'sort' => $product['sort'] ?? 0, 'add_time' => $time ]; try { $path = root_path() . 'public' . $download->thumb(true)->downloadImage($data['cover_img'])['path']; $coverImgUrl = $miniUpload->uploadImage($path)->media_id; @unlink($path); } catch (\Throwable $e) { Log::error('添加直播商品图片错误,原因:' . $e->getMessage()); $coverImgUrl = $data['cover_img']; } $res = MiniProgramService::addGoods($coverImgUrl, $data['name'], $data['price_type'], $data['url'], floatval($data['price'])); $data['goods_id'] = $res['goodsId']; $data['audit_id'] = $res['auditId']; $data['audit_status'] = 1; $dataAll[] = $data; } if (!$goods = $this->dao->saveAll($dataAll)) { throw new AdminException('添加商品失败'); } return true; }
继续跟踪$goods_info
变量,函数将$goods_info
数组中的信息赋值给$data
foreach ($goods_info as $product) { $data = [ 'product_id' => $product['id'], 'name' => Str::substrUTf8($product['store_name'], 12, 'UTF-8', ''), 'cover_img' => $product['image'] ?? '', 'price_type' => 1, 'cost_price' => $product['cost_price'] ?? 0.00, 'price' => $product['price'] ?? 0.00, 'url' => 'pages/goods_details/index?id=' . $product['id'], 'sort' => $product['sort'] ?? 0, 'add_time' => $time ];
继续往下看,将$data
数组的cover_img
值传递给downloadImage
函数,继续跟进
$path = root_path() . 'public' . $download->thumb(true)->downloadImage($data['cover_img'])['path'];
来到文件crmeb/services/DownloadImageService.php
,函数downloadImage
源码如下:
public function downloadImage(string $url, $name = '') { if (!$name) { //TODO 获取要下载的文件名称 $downloadImageInfo = $this->getImageExtname($url); $name = $downloadImageInfo['file_name']; if (!$name) throw new ValidateException('上传图片不存在'); } if (strstr($url, 'http://') === false && strstr($url, 'https://') === false) { $url = 'http:' . $url; } $url = str_replace('https://', 'http://', $url); if ($this->path == 'attach') { $date_dir = date('Y') . DIRECTORY_SEPARATOR . date('m') . DIRECTORY_SEPARATOR . date('d'); $to_path = $this->path . '/' . $date_dir; } else { $to_path = $this->path; } $upload = UploadService::init(1); if (!file_exists($upload->uploadDir($to_path) . '/' . $name)) { ob_start(); readfile($url); $content = ob_get_contents(); ob_end_clean(); $size = strlen(trim($content)); if (!$content || $size <= 2) throw new ValidateException('图片流获取失败'); if ($upload->to($to_path)->down($content, $name) === false) { throw new ValidateException('图片下载失败'); } $imageInfo = $upload->getDownloadInfo(); $path = $imageInfo['dir']; if ($this->thumb) { Image::open(root_path() . 'public' . $path)->thumb($this->thumbWidth, $this->thumHeight)->save(root_path() . 'public' . $path); $this->thumb = false; } } else { $path = '/uploads/' . $to_path . '/' . $name; $imageInfo['name'] = $name; } $date['path'] = $path; $date['name'] = $imageInfo['name']; $date['size'] = $imageInfo['size'] ?? ''; $date['mime'] = $imageInfo['type'] ?? ''; $date['image_type'] = 1; $date['is_exists'] = false; return $date; }
可控变量$data['cover_img']
作为参数传递给$url
,继续跟踪$url
,函数从$rul
指定的地址获取文件内容,并保存在变量$content
中
ob_start();readfile($url);$content = ob_get_contents();ob_end_clean();
跟踪$content
,将$content
传入了函数$down
$upload->to($to_path)->down($content, $name)
来到文件crmeb/services/upload/storage/Local.php
,函数down
源码如下:
public function down(string $fileContent, string $key = null) { if (!$key) { $key = $this->saveFileName(); } $dir = $this->uploadDir($this->path); if (!$this->validDir($dir)) { return $this->setError('Failed to generate upload directory, please check the permission!'); } $fileName = $dir . '/' . $key; file_put_contents($fileName, $fileContent); $this->downFileInfo->downloadInfo = new File($fileName); $this->downFileInfo->downloadRealName = $key; $this->downFileInfo->downloadFileName = $key; $this->downFileInfo->downloadFilePath = $this->defaultPath . '/' . $this->path . '/' . $key; return $this->downFileInfo; }
继续跟踪$fileContent
,函数将$fileContent
的内容写入到文件
$fileName
中
file_put_contents($fileName, $fileContent);
现在再来看看$fileNmae
的值,回到文件crmeb/services/DownloadImageService.php
的函数downloadImage
,$url
是我们可以控制的值,传递给了本类的getImageExtname
函数
$downloadImageInfo = $this->getImageExtname($url);
getImageExtname
源码如下,大概意思就是将$url
链接进行md5加密后作为文件的新名字复制给file_name
然后返回给downloadImage
函数:
public function getImageExtname($url = '', $ex = 'jpg') { $_empty = ['file_name' => '', 'ext_name' => $ex]; if (!$url) return $_empty; if (strpos($url, '?')) { $_tarr = explode('?', $url); $url = trim($_tarr[0]); } $arr = explode('.', $url); if (!is_array($arr) || count($arr) <= 1) return $_empty; $ext_name = trim($arr[count($arr) - 1]); $ext_name = !$ext_name ? $ex : $ext_name; return ['file_name' => md5($url) . '.' . $ext_name, 'ext_name' => $ext_name]; }
downloadImage
函数将返回的file_name
值赋值给变量$name
$name = $downloadImageInfo['file_name'];
然后将$name
作为第二个参数传入down
$upload->to($to_path)->down($content, $name)
回到down
函数,将传入的$name
作为参数$key
值拼接到变量$dir
作为文件的位置,这样一来我们就可以控制函数file_put_contents
的内容并且知道文件的位置
$fileName = $dir . '/' . $key;
但是有个问题,回到文件app/services/activity/live/LiveGoodsServices.php
的函数add
中发现,我们最后存储的文件会被使用@unlink($path)
删除,这里可以通过不配置微信的appid
在执行$miniUpload->uploadImage($path)->media_id;
时抛出异常来跳过@unlink($path)
的执行,执行catch里的代码
try { $path = root_path() . 'public' . $download->thumb(true)->downloadImage($data['cover_img'])['path']; $coverImgUrl = $miniUpload->uploadImage($path)->media_id; @unlink($path); } catch (\Throwable $e) { Log::error('添加直播商品图片错误,原因:' . $e->getMessage()); $coverImgUrl = $data['cover_img']; }
本地搭建环境后登陆后台
在服务器上放上恶意代码并开启文件下载服务
进入后台,如果下面页面有设置appid则将其设置为空
进入到后台的直播商品管理界面
点击添加商品,选择商品后提交抓包,更改image参数为我们服务器上恶意文件地址
然后将服务器文件地址进行md5
访问路径如下:
http://domain.com/uploads/attach/{year}/{month} /day}/{远程文件url的md5编码}.php
成功执行代码
来源地址:https://blog.csdn.net/heartself/article/details/127522470
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341