CRMEB-BZ:图片转 Base64 / Phar 反序列化 WebShell — 漏洞说明与修改明细
============================================================
路径均相对站点 crmeb/ 根目录(与修复包内路径一致)。
----------------------------------------
一、修改了哪些文件(总览)
----------------------------------------
序号 | 修改文件 | 改动类型 | 修改位置(方法 / 函数) | 改了什么
1 | app/api/controller/v1/PublicController.php | 重写方法 | get_image_base64() | 白名单校验、去掉错误 phar 判断、去掉 put_image 回退
2 | app/common.php | 新增函数 | normalize_image_host() | 规范化域名 host
3 | app/common.php | 新增函数 | image_url_is_allowed() | 校验 URL 协议与白名单
4 | app/common.php | 新增函数 | image_ip_in_range() | CIDR 判断
5 | app/common.php | 新增函数 | image_ip_is_public() | 判断是否公网 IP
6 | app/common.php | 新增函数 | resolve_public_image_host() | DNS 解析并拒绝内网 IP
7 | app/common.php | 新增函数 | image_data_to_base64() | 校验真实图片 MIME 后转 Data URI
8 | app/common.php | 重写函数 | image_to_base64() | 安全读取本站/远端图片
9 | app/common.php | 重写函数 | put_image() | 不再对 URL 直接 file_get_contents
修复包内仅含上述 2 个文件:
crmeb/app/api/controller/v1/PublicController.php
crmeb/app/common.php
----------------------------------------
二、漏洞简述
----------------------------------------
攻击者上传恶意 JPG 后,调用 POST /api/image_base64。旧代码在失败时走 put_image() → file_get_contents($url),可触发 Phar 反序列化并写出 x2.php 等后门。
----------------------------------------
三、按文件 / 按位置的详细修改
----------------------------------------
文件 1:app/api/controller/v1/PublicController.php
位置:get_image_base64() 方法
项目 | 说明
5 月约行号 | 约第 327 行起
修复后约行号 | 约第 354 行起
调用路由 | POST /api/image_base64(app/api/route/v1.php)
修改点 A — 域名白名单构造(方法前半段)
* 改前:合并对象存储域名 + $request->host(),用 str_replace 去 http(s)://;再拼 mp.weixin.qq.com。
* 改后:合并对象存储域名 + sys_config('site_url') + mp.weixin.qq.com,用 normalize_image_host() 得到纯 host 列表。
* 原因:避免 Host 头干扰;统一 host 比较。
修改点 B — URL 合法性判断(白名单校验处)
* 改前:只比较 parse_url(...)['host'] 是否在数组中;另有一段错误逻辑:
// 仅当「扩展名不像图片」并且「URL 含 phar://」时才拒绝
if ($imageUrl !== '' && !preg_match('/.*(\.png|\.jpg|\.jpeg|\.gif)$/', $imageUrl)
&& strpos(strtolower($imageUrl), "phar://") !== false) { ... }
* 改后:删除上述 phar 判断;改为:
if (($imageUrl !== '' && !image_url_is_allowed($imageUrl, $domainArr))
|| ($codeUrl !== '' && !image_url_is_allowed($codeUrl, $domainArr))) {
return app('json')->success(['code' => false, 'image' => false]);
}
* 原因:旧判断对 .jpg 结尾的恶意 URL 无效;新逻辑只允许 http/https + 白名单 host。
修改点 C — 取图与缓存(try 代码块内)
* 改前:CacheService::remember($url, ...);先 image_to_base64,失败再 put_image($url),再读本地转 base64 并 unlink。
* 改后:缓存键改为 'safe_image_base64:' . hash('sha256', $url);只调用 image_to_base64($url, 9, $domainArr);不再调用 put_image。
* 原因:切断「失败后对任意 URL 做 file_get_contents」的利用路径。
修改点 D — 异常捕获
* 改前:catch (\Exception $e)
* 改后:catch (\Throwable $e),仍返回「操作失败」。
----------------------------------------
文件 2:app/common.php
位置 A(新增):约第 547–719 行,插在 path_to_url() 与原 image_to_base64() 之间
新增函数 | 约行号 | 修改内容
normalize_image_host() | ~552 | 新增。从域名/URL 解析小写 host
image_url_is_allowed() | ~579 | 新增。限制 scheme 为 http/https,禁止 userinfo,host 必须在白名单
image_ip_in_range() | ~605 | 新增。CIDR 匹配
image_ip_is_public() | ~631 | 新增。拒绝私网/保留地址及常见绕过网段
resolve_public_image_host() | ~651 | 新增。解析 DNS,要求全部 IP 为公网
image_data_to_base64() | ~707 | 新增。getimagesizefromstring 校验 MIME 后生成 Data URI,限 10MB
位置 B(重写):image_to_base64()
项目 | 说明
5 月约行号 | 约第 552 行起
修复后约行号 | 约第 729 行起
具体改动:
1. 函数签名
* 改前:image_to_base64($avatar = '', $timeout = 9)
* 改后:增加第三参数 array $allowedHosts = []
2. 入口校验(函数开头)
* 新增:非字符串 / 空串直接 false;若传入 $allowedHosts 则先 image_url_is_allowed。
3. URL 解析段
* 改前:str_replace('https', 'http', $avatar) 强制降级。
* 改后:必须有合法 scheme/host;scheme ∈ {http, https};禁止 userinfo;去掉强制降级。
4. 本站文件读取段(与 site_url 同 host 时)
* 改前:file_get_contents(public_path() . substr($url['path'], 1)),再用正则删 <?php...?>。
* 改后:realpath + 必须落在 public/ 目录内 + 大小限制 + image_data_to_base64 校验真实图片。
5. 远端 curl 段
* 改前:CURLOPT_FOLLOWLOCATION = true,CURLOPT_SSL_VERIFYPEER = false,结果固定当 jpeg。
* 改后:先 resolve_public_image_host;FOLLOWLOCATION = false;限制协议为 HTTP/HTTPS;校验证书;限 10MB;用 image_data_to_base64。
位置 C(重写):put_image()
项目 | 说明
5 月约行号 | 约第 602 行起
修复后约行号 | 约第 833 行起
具体改动:
改前(危险点) | 改后
$imgData = file_get_contents($url); | 改为 image_to_base64($url)
正则去掉 PHP 标签后直接落盘 | 仅接受合法 data:image/...;base64,...,按 MIME 选扩展名
文件名较多依赖 URL 扩展名 | 文件名过滤为安全字符,目录固定 uploads/qrcode/
原因:此处直接 file_get_contents($url) 是帖子攻击链高度吻合的触发点。
----------------------------------------
四、修改位置速查(给实施同学)
----------------------------------------
打开修复后源码,按下列符号核对是否已打上补丁:
PublicController.php
└─ get_image_base64()
├─ normalize_image_host / image_url_is_allowed ← 应存在
├─ 旧的 preg_match + phar:// 判断 ← 应已删除
└─ put_image( 回退调用 ← 应已删除
common.php
├─ normalize_image_host / image_url_is_allowed / ... ← 应新增
├─ image_to_base64(..., array $allowedHosts = []) ← 应有第三参数
└─ put_image()
└─ file_get_contents($url) ← 应已不存在
----------------------------------------
五、如何覆盖
----------------------------------------
1. 备份原文件。
2. 解压 zip,将包内两个文件按路径覆盖到站点 crmeb/。
3. 清缓存 / 重启 PHP-FPM。
4. 建议禁止 uploads 目录执行 PHP;排查 x2.php、.sys_cache.php。
无数据库脚本。
----------------------------------------
下载附件传到到服务器覆盖app文件

