在Web安全领域,PHP反序列化漏洞一直是CTF竞赛中的热门考点,也是真实业务场景中极具威胁的攻击面。今天,我们将通过一道经典的ctf show题目,完整剖析从代码审计、漏洞触发到最终获取Flag的整个攻击链路。无论你是刚入门的安全爱好者,还是有经验的开发者,这篇文章都将为你提供全新的视角。
一、漏洞初现:代码审计与陷阱识别
拿到题目后,第一步自然是进行源码审计。经过快速梳理,我们很快定位到一个明显的反序列化入口。然而,这道题并非简单的直球攻击,它巧妙地设置了一个陷阱——目标类中定义了 __wakeup() 魔术方法。在PHP反序列化机制中,__wakeup() 会在对象被反序列化时立即自动调用,这直接导致我们无论构造多么精巧的Payload,最终都会被强制重置为“error”状态,攻击似乎陷入了僵局。
让我们先看一下这段关键的代码逻辑:

这里需要引入一个PHP版本演进的重要知识点。在PHP 7.4及更高版本中,如果同一个类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,那么反序列化时只会执行 __unserialize(),而 __wakeup() 会被完全忽略。这意味着,我们找到了绕过陷阱的突破口!

⚠️ 技术延伸:这种特性变更类似于其他语言中的版本兼容性问题。例如,在 Java 的序列化机制中,readObject() 的优先级也高于自定义的 validateObject();而在 Go 语言中,由于没有内置的序列化魔术方法,开发者通常通过显式接口来控制反序列化行为。理解不同语言间的设计哲学差异,有助于我们更快地定位漏洞点。
二、深挖触发点:从__invoke到__destruct
既然绕过了 __wakeup() 的干扰,我们继续寻找真正的代码执行触发点。很多新手第一眼会关注到 __invoke() 方法,因为它在对象被当函数调用时触发,看起来非常适合执行恶意代码。但仔细审计后会发现,整个代码库中并没有形如 $obj() 的调用方式,因此 __invoke() 在这里是死路一条。
$obj = new ctfshowvip();
$obj(); // 像调用函数一样调用对象,此时会触发 __invoke()
真正的答案隐藏在对象生命周期管理的底层逻辑中——__destruct() 析构函数。当对象被销毁(例如脚本执行完毕或引用计数归零)时,PHP会自动调用该方法。在本题中,__destruct() 内部存在一个 file_put_contents() 调用,这正是我们梦寐以求的“写文件”能力。只要我们能控制写入的文件名和内容,就能轻松植入Webshell。

这就像在 TypeScript 或 C++ 中,我们不仅要关注构造函数,还要留意析构函数或资源释放回调中可能存在的副作用。攻击面往往藏在那些“看似无害”的生命周期钩子里。
三、弱类型比较的巧妙利用:绕过数字校验
找到了写入函数,但还有一个硬性门槛:__unserialize() 中强制校验 $this->code == 877(十六进制0x36d)。我们来看一下code的生成逻辑:
$this->code = $this->username . $this->password;
这是一个简单的字符串拼接。假设我们将 username 设为文件名(如 877shell.php),将 password 设为恶意代码(如 <?php eval($_POST[1]);?>),那么拼接后的code字符串必然包含大量字母(例如 877shell.php<?php...)。一个包含字母的字符串,如何通过 == 877 的数字比较呢?
这里利用的是PHP弱类型比较的特性:当字符串与数字进行 == 比较时,PHP会尝试将字符串转换为数字。转换规则如下:
- ✅ 如果字符串以数字开头,则转换为开头的数字部分
- ✅ 如果字符串不是以数字开头,则转换为整数0
因此,只要code字符串以 877 开头,在与数字877进行比较时,PHP就会判定它们相等。这个特性类似于 JavaScript 中的隐式类型转换(例如 "877abc" == 877 返回true),但在 Go 或 Java 这类强类型语言中则完全不可能发生。理解语言特性之间的差异,是跨语言安全研究的基础。
四、Payload构造与实战利用
现在所有障碍都已清除,我们可以开始手工构造最终的序列化Payload了。核心思路是:构造一个符合目标类结构的对象,确保 username 为 877shell.php,password 为 <?php eval($_POST[1]);?>,并确保 code 属性在反序列化后满足弱类型比较条件。
<?php
class ctfshowvip {
public $username;
public $password;
}
$a = new ctfshowvip();
$a->username = "877shell.php";
// 注意:如果后端对某些敏感字符有限制,可以利用 base64 编码或取反,
// 但此处 file_put_contents 直接写入,我们直接放一句话木马
$a->password = "<?php @eval($_POST[1]);?>";
echo serialize($a);
?>
将生成的序列化字符串作为 vip 参数提交给服务器:

请求发送后,页面返回一个空白响应。这实际上是成功执行的标志——说明 file_put_contents() 已在当前目录下成功创建了名为 877shell.php 的文件。

接下来,我们使用中国蚁剑(AntSword)等工具连接这个Webshell:


成功连接后,我们在服务器的根目录下发现了 flag_is_here 文件,Flag就藏身其中。最终获取到的Flag值为:ctfshow{ea123419-fc63-455f-b448-e2085a88d762}。至此,整个攻击链完美闭环。
[AFFILIATE_SLOT_1]五、总结与防御建议
这道CTF题目虽然小巧,但五脏俱全,它完美串联了PHP反序列化漏洞的多个核心考点:魔术方法优先级、弱类型比较、对象生命周期钩子利用等。通过这次实战,我们不仅掌握了一条完整的攻击链构造方法,更深刻理解了PHP底层设计的一些“反直觉”特性。
对于开发者而言,防范此类漏洞的最佳实践包括:
- ✅ 避免对用户输入直接进行反序列化,始终使用白名单类校验
- ✅ 对反序列化后的对象属性进行严格的类型和内容校验
- ✅ 使用强类型比较(===)替代弱类型比较(==)
- ✅ 在类和函数层面做好权限隔离,降低被利用后的影响范围
安全是一场持续的攻防博弈,理解攻击者的思维,才能构建更坚固的防御体系。希望这篇文章能为你打开PHP反序列化研究的大门,在后续的学习中举一反三。
[AFFILIATE_SLOT_2]
浙公网安备 33010602011771号