CVE-2024-6944 CRMEB电商系统 反序列化漏洞复现
前言
该漏洞是存在于钟邦科技 CRMEB 5.4.0 版本中的一个关键安全漏洞,影响到 PublicController.php
文件中的 get_image_base64
函数。该漏洞的根本原因是由于对用户可控的参数 code
进行处理时,存在不安全的反序列化操作的风险。攻击者可以利用此漏洞,通过发送特制的请求,触发 PHP 的反序列化机制,从而执行恶意代码,最终可能导致远程代码执行(RCE)的严重后果。该漏洞已被公开披露,并且可能被恶意利用。
什么是反序列化漏洞
在计算机科学中,序列化 (Serialization) 是将一个对象(例如,PHP 中的一个类实例)转换成可以存储或传输的数据格式的过程。这种格式通常是字符串或字节流。
反序列化 (Deserialization) 则是序列化的逆过程,它将存储或传输的数据格式转换回原始的对象。
为什么需要序列化和反序列化?
序列化和反序列化在很多场景下都非常有用,例如:
- 存储数据: 将复杂的数据结构(如对象)保存到文件或数据库中。
- 传输数据: 在网络上传输对象,例如在 API 请求或会话管理中。
- 缓存: 将对象存储在缓存中以提高性能。
什么是反序列化漏洞?
反序列化漏洞发生在应用程序在反序列化来自不可信来源的数据时,没有进行充分的验证和过滤,导致恶意攻击者能够控制反序列化的过程,从而执行恶意代码或进行其他恶意操作。
核心问题在于,反序列化过程会根据序列化数据中包含的信息来重建对象。如果攻击者能够控制这些信息,他们就可以在反序列化过程中注入恶意的代码或指令。
PHP 反序列化漏洞与 RCE
PHP 提供了 serialize()
函数用于序列化数据,以及 unserialize()
函数用于反序列化数据。PHP 的反序列化漏洞是比较常见且危害较大的漏洞之一,尤其容易导致远程代码执行(RCE)。
PHP 中导致反序列化漏洞的关键在于其“魔术方法 (Magic Methods)”:
这些是在特定事件发生时自动调用的特殊方法。一些常见的与反序列化漏洞相关的魔术方法包括:
__wakeup()
: 在反序列化时被调用。__destruct()
: 在对象被销毁时被调用。__toString()
: 当对象被当作字符串处理时被调用。__call()
: 在调用对象中不存在的方法时被调用。__get()
/__set()
: 在访问或设置对象中不存在的属性时被调用。
PHP 反序列化实现 RCE 的原理
攻击者可以通过构造恶意的序列化数据,使其在反序列化过程中触发某些魔术方法,并在这些魔术方法中执行他们预先设定的恶意代码。
举个简单的 PHP RCE 示例
假设我们有以下 PHP 代码:
PHP
|
在这个例子中:
- 我们定义了一个名为
Evil
的类,它有一个公共属性$cmd
。 - 该类中定义了一个魔术方法
__destruct()
。当Evil
类的对象被销毁时,__destruct()
方法会被自动调用,并执行$this->cmd
中存储的命令。 - 脚本通过 GET 请求接收一个名为
data
的参数,该参数经过 Base64 解码后被unserialize()
函数反序列化。
漏洞原理分析
get_image_base64
首先我们来分析一下漏洞函数
/** |
进行代码审计可以发现,
get_image_base64
函数接收两个 POST 参数:image
和code
,并将它们分别赋值给$imageurl
和$codeUrl
变量- 函数对这两个参数进行了初步的校验,要求它们必须包含
.png
,.jpg
,.jpeg
, 或.gif
等图片扩展名,并且$imageurl
不允许包含phar://
字段 - 接下来,代码尝试使用
CacheService::remember()
函数缓存$codeUrl
对应的 base64 编码的图片。如果缓存不存在,则执行一个匿名函数,该匿名函数会依次调用image_to_base64($codeUrl)
和put_image($codeUrl)
put_image
function image_to_base64($avatar = '', $timeout = 9) |
function put_image($url, $filename = '') |
image_to_base64
函数: 此函数以$codeUrl
为参数发起一个 cURL 请求,尝试获取图片内容并进行 base64 编码。关键在于,当 cURL 处理phar://
协议的 URL 时,会返回false
put_image
函数(漏洞触发点):put_image
函数接收$codeUrl
作为参数,并尝试将其中出现的phar://
替换为空字符串:$url = str_replace('phar://', '', $url);
。然而,通过双写phar://
(例如phar://phar://...
) 可以绕过此替换。 之后,函数会调用readfile($url);
。当$url
以phar://
开头时,readfile
函数会将该路径视为 Phar 归档文件,并尝试读取,从而触发 Phar 反序列化
漏洞利用链
寻找反序列化 Gadget
- 原理: 在 PHP 中,反序列化漏洞本身并不能直接执行任意代码。它需要结合已存在于项目代码或依赖库中的某些类(称为 “Gadget”)来实现。这些 Gadget 类中存在特定的“魔术方法”(如
__destruct()
、__wakeup()
等),在对象被反序列化或销毁等特定时机被自动调用 - 在这个漏洞中: 我们通过搜索
__destruct()
方法,在 GuzzleHTTP 库的FileCookieJar
类中找到了一个可利用的 Gadget - 为什么
FileCookieJar
可利用:FileCookieJar
类的__destruct()
方法会调用自身的save()
方法。而save()
方法的功能是将 Cookie 信息写入到指定的文件中。这为攻击者提供了一个可控的文件写入操作
构造恶意 Phar 文件
- 原理: Phar 文件是 PHP 的归档文件,可以包含多个 PHP 文件和元数据。元数据可以被序列化存储。当 PHP 处理
phar://
协议的文件时,会尝试反序列化其元数据 - 在这个漏洞中:我们需要创建一个恶意的 Phar 文件。这个文件的元数据中包含一个序列化后的
GuzzleHttp\Cookie\FileCookieJar
对象 - 恶意对象的关键属性:
$filename
属性被设置为攻击者想要写入 Webshell 的目标路径,例如public/shell.php
$cookies
属性被设置为包含恶意 PHP 代码的 Cookie 信息。当FileCookieJar
对象的save()
方法被调用时,这些恶意代码就会被写入到$filename
指定的文件中
绕过黑名单检测
- 原理: 为了防止恶意文件的上传,系统通常会对上传的文件进行检查,例如检查文件名后缀、文件内容等。可能会存在对包含特定字符串(如常见的反序列化类名)的文件内容的黑名单检测
- 在这个漏洞中: 为了绕过这种内容检测,攻击者可以使用
gzip
压缩 Phar 文件。压缩后的文件内容会变得难以直接识别
上传 Phar 文件
- 原理: 攻击者需要找到一个允许上传文件的入口点
- 在这个漏洞中:可以通过注册用户后上传头像的功能来实现 Phar 文件的上传。上传成功后,攻击者会获得上传文件在服务器上的路径
触发反序列化
原理: 要执行 Phar 文件中的恶意操作,需要触发 PHP 对其元数据的反序列化。这通常通过使用
phar://
协议来访问 Phar 文件来实现在这个漏洞中:
攻击者构造一个 POST 请求发送到
/api/image_base64
接口。关键在于
code
参数的值。
code
参数的值需要指向之前上传的 Phar 文件的 URL- 双写
phar://
绕过: 由于put_image
函数中存在将phar://
替换为空的逻辑,攻击者使用双写phar://
(例如phar://phar:///path/to/uploaded/compressed_phar_file.jpg.gz
) 来绕过这个替换。这样,经过一次替换后,readfile
函数接收到的仍然是以phar://
开头的路径 readfile
触发反序列化:get_image_base64
函数最终会调用put_image
,put_image
函数在经过绕过处理后,会使用readfile()
函数来读取$codeUrl
指向的文件。当readfile()
函数处理以phar://
开头的路径时,PHP 会将该文件识别为 Phar 归档文件,并自动反序列化其元数据
执行恶意代码:
- 原理: 当 Phar 文件的元数据(即序列化的
FileCookieJar
对象)被反序列化后,PHP 会自动调用该对象的__destruct()
方法 - 在这个漏洞中:
FileCookieJar
对象的__destruct()
方法被调用,接着会调用其save()
方法。save()
方法会将$cookies
属性中存储的恶意 PHP 代码写入到$filename
属性指定的路径(例如public/shell.php
)
POC
<?php |
namespace{ ... }
: 这部分代码是生成 Phar 文件的主要逻辑
$exp = new GuzzleHttp\Cookie\FileCookieJar();
创建了一个 FileCookieJar
类的实例 $exp
。这个对象包含了恶意代码和目标文件路径
$phar = new Phar('test.phar');
创建一个新的 Phar 对象,文件名为 test.phar
$phar -> stopBuffering();
停止缓冲,准备写入文件内容
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
设置 Phar 文件的 Stub。Stub 是 Phar 文件的入口点,这里使用了一个 GIF 图片的文件头 GIF89a
,后面跟着 <?php __HALT_COMPILER(); ?>
。__HALT_COMPILER()
用于标识 Phar 文件内容的结束。使用 GIF 文件头是为了在上传时绕过可能隐含的文件类型检查
$phar -> addFromString('test.txt','test');
向 Phar 文件中添加一个名为 test.txt
的虚拟文件,内容为 test
。这是 Phar 文件必需的部分,但内容本身在这个漏洞中并不重要
$phar -> setMetadata($exp);
这是最关键的一行。它将之前创建的包含恶意信息的 $exp
对象进行序列化,并将其设置为 Phar 文件的元数据。 当 Phar 文件被以 phar://
协议处理时,PHP 会自动反序列化这个元数据。
$phar -> stopBuffering();
停止写入 Phar 文件
rename('test.phar','test.jpg');
将生成的 test.phar
文件重命名为 test.jpg
。这是为了在上传时绕过文件扩展名检查