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
19.Enter the background, if the following page has set appid, set it to empty
20.Enter the live broadcast product management interface in the background
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
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
19.Enter the background, if the following page has set appid, set it to empty
20.Enter the live broadcast product management interface in the background
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
22.Then md5 encode the server file address
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: