Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An arbitrary file download vulnerability exists #63

Closed
Nmslgkd opened this issue Oct 25, 2022 · 0 comments
Closed

An arbitrary file download vulnerability exists #63

Nmslgkd opened this issue Oct 25, 2022 · 0 comments

Comments

@Nmslgkd
Copy link

Nmslgkd commented Oct 25, 2022

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 $name0
$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
image-20221025030612377

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
image-20221025030807714

22.Then md5 encode the server file address
image-20221025030938698

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

@Nmslgkd Nmslgkd closed this as not planned Won't fix, can't repro, duplicate, stale Oct 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant