crmeb / CRMEB

🔥CRMEB开源商城免费开源多语言商城系统,Tp6框架商城,系统可商用;包含小程序商城、H5商城、公众号商城、PC商城、App,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面DIY,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议
http://www.crmeb.com
Apache License 2.0
6.17k stars 1.53k forks source link

There is an arbitrary file download vulnerability exists #64

Closed Nmslgkd closed 1 year ago

Nmslgkd commented 2 years ago

1.After downloading the source code, go to the add function in the app/adminapi/controller/v1/marketing/live/LiveGoods.php file. `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('添加成功');

}`

2.The function accepts a goods_info parameter from the front end and assigns it to the variable $goods_info [$goods_info] = $this->request->postMore([ ['goods_info', []] ], true);

3.Enter the add function of the $services object by tracking the $goods_info parameter $this->services->add($goods_info);

4.In this class you can see services declared as class LiveGoodsServices public function construct(App $app, LiveGoodsServices $services) { parent::construct($app); $this->services = $services; }

5.In this class, you can see that services is declared as class LiveGoodsS to the file app/services/activity/live/LiveGoodsServices.php, and the source code of the function add is as follows: services public function add(array $goods_info) { $product_ids = array_column($goods_info, 'id'); $this->create($product_ids); $miniUpload = MiniProgramService::materialTemporaryService(); /* @var DownloadImageService $download / $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; }`

6.Continue to track the $goods_info variable, the function assigns the information in the $goods_info array to $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 ];

7.Continue reading down, pass the cover_img value of the $data array to the downloadImage function, and follow up $path = root_path() . 'public' . $download->thumb(true)->downloadImage($data['cover_img'])['path'];

8.Go to the file crmeb/services/DownloadImageService.php, the source code of the function downloadImage is as follows: 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; }

9.The controllable variable $data['cover_img'] is passed as a parameter to $url, continue to track $url, the function obtains the file content from the address specified by $rul, and saves it in the variable $content ob_start(); readfile($url); $content = ob_get_contents(); ob_end_clean();

10.Track $content, pass $content to function $down $upload->to($to_path)->down($content, $name)

11.Go to the file crmeb/services/upload/storage/Local.php, the source code of the function down is as follows: 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; }

12.Continue to track $fileContent, the function writes the contents of $fileContent to the file $fileName file_put_contents($fileName, $fileContent);

13.Now let's take a look at the value of $fileNmae, go back to the function downloadImage of the file crmeb/services/DownloadImageService.php, $url is a value we can control, passed to the getImageExtname function of this class $downloadImageInfo = $this->getImageExtname($url);

14.The source code of getImageExtname is as follows, which probably means that the $url link is encrypted by md5 and then copied to file_name as the new name of the file and then returned to the downloadImage function: 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]; }

15.The downloadImage function assigns the returned value of file_name to the variable $name $name = $downloadImageInfo['file_name'];

16.Go back to the down function, splicing the incoming $name as the parameter $key value to the variable $dir as the location of the file, so that we can control the content of the function file_put_contents and know the file s position $fileName = $dir . '/' . $key;

17.But there is a problem, go back to the function add of the file app/services/activity/live/LiveGoodsServices.php and find that the last file we stored will be deleted using @unlink($path), here you can pass appid without WeChat configuration throws an exception when executing $miniUpload->uploadImage($path)->media_id; to skip the execution of @unlink($path) and execute the code in the 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']; } 18.After setting up the environment locally, log in to the background,Put malicious code on the server and start the file download service 1666705698348

19.Enter the background, if the following page has set appid, set it to empty image-20221025034814652

20.Enter the live broadcast product management interface in the background image-20221025025001348

21.Click to add a product, select the product and submit the packet capture, change the image parameter to the malicious file address on our server 1666705814499

22.Then md5 encode the server file address 1666706426300

23.The access path is as follows:http://domain.com/uploads/attach/{year}/{month}/{day}/{md5 encoding of remote file url}.php Code executed successfully: image-20221025031209417

fengmangbl commented 1 year ago

这是来自QQ邮箱的自动回复邮件。   您好,我不在电脑前,无法亲自回复您的邮件。我将尽快给您回复。