一、什么是序列化?大白话解释
序列化 = 把变量(数组、对象等)转成字符串,方便存储或传输
反序列化 = 把字符串还原成原来的变量
举个例子:你要把一个对象存到数据库里,但数据库只认字符串,那就先序列化成字符串,存进去;下次取出来时,再反序列化还原成对象。
核心函数:
serialize() // 对象 → 字符串
unserialize() // 字符串 → 对象
二、序列化格式速查
基本类型
| 类型 | 格式 | 示例 |
|---|---|---|
| 字符串 | s:长度:"内容" |
s:5:"hello" |
| 整数 | i:数值 |
i:18 |
| 浮点数 | d:数值 |
d:3.14 |
| 布尔值 | b:0或1 |
b:1 |
| 数组 | a:长度:{...} |
a:2:{i:0;s:3:"abc";i:1;i:123;} |
| 对象 | O:类名长度:"类名":属性数:{...} |
O:6:"Person":2:{...} |
| NULL | N |
N |
实例:序列化一个字符串
<?php
echo serialize('derry');
// 输出: s:5:"derry";
?>
• s = string(字符串)
• 5 = 长度是5个字符
• "derry" = 内容
三、无类序列化漏洞实战
案例1:最简单的反序列化漏洞
代码:
<?php
error_reporting(0);
include "flag.php";
$KEY = "derry";
$str = $_GET['x'];
if(unserialize($str) === $KEY) {
echo "$flag" ."</br>";
}
show_source(__FILE__);
?>
分析 :只要我们传入的 x 反序列化后等于 "derry",就能拿到 flag!
解题步骤:
1. 先把 "derry" 序列化:
<?php
echo serialize('derry');
// 输出: s:5:"derry";
?>
1. 构造URL:
http://127.0.0.1/1.php?x=s:5:"derry";
拿到flag!
案例2:CTF真题(Cookie传参)
代码:
<?php
error_reporting(0);
include_once("flag.php");
$cookie = $_COOKIE['ISecer'];
if(isset($_GET['hint'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY") {
echo "$flag";
}
// ... 省略HTML ...
$KEY = 'ISecer:www.isecer.com';
?>
分析:
• 数据从 Cookie 的 ISecer 字段传入
• 反序列化后要等于 $KEY,但 $KEY 定义在代码最后
• PHP 的变量定义顺序不影响判断,关键是 $KEY 的值
解题步骤:
方法一:正常序列化
<?php
echo serialize('ISecer:www.isecer.com');
// 输出: s:21:"ISecer:www.isecer.com";
?>
用 Burp Suite 抓包,修改 Cookie:
Cookie: ISecer=s:21:"ISecer:www.isecer.com";
方法二:利用变量未定义漏洞
由于 $KEY 在使用后才定义,此时 $KEY 其实是 NULL 或空字符串!
<?php
echo serialize("");
// 输出: s:0:"";
?>
修改 Cookie:
Cookie: ISecer=s:0:"";
成功拿到flag!
四、类序列化:对象也能序列化
序列化一个对象
<?php
class Person {
public $name = 'derry';
public $age = 18;
}
$p = new Person();
$obj = serialize($p);
print_r($obj);
?>
输出:
O:6:"Person":2:{s:4:"name";s:5:"derry";s:3:"age";i:18;}
解析:
• O = Object(对象)
• 6:"Person" = 类名长度6,类名是Person
• 2 = 有2个属性
• {s:4:"name";s:5:"derry";s:3:"age";i:18;} = 属性列表
反序列化还原对象
<?php
class Person {
public $name = 'derry';
public $age = 18;
}
$clz = 'O:6:"Person":2:{s:4:"name";s:8:"derry666";s:3:"age";i:18;}';
$per = unserialize($clz);
echo $per->name; // 输出: derry666
?>
注意:反序列化时,类必须存在,否则会失败!
五、魔术方法:反序列化的关键
魔术方法是PHP的特殊方法,以 __ 开头,在特定时机自动调用。反序列化漏洞常利用这些方法!
1. __construct() 和 __destruct()
<?php
class Person {
public $name = 'derry';
function __construct() {
echo "对象创建时调用 __construct<br>";
}
function __destruct() {
echo "对象销毁时调用 __destruct<br>";
}
}
$a = new Person(); // 触发 __construct
echo serialize($a); // 序列化
// 脚本结束时触发 __destruct
?>
2. __sleep() 和 __wakeup()
这是反序列化漏洞最常用的两个方法!
<?php
class Person {
public $name = 'derry';
function __sleep() {
echo "序列化前调用 __sleep<br>";
return array("name"); // 返回要序列化的属性名
}
function __wakeup() {
echo "反序列化后调用 __wakeup<br>";
}
}
$a = new Person();
echo serialize($a); // 触发 __sleep
$b = 'O:6:"Person":1:{s:4:"name";s:5:"derry";}';
unserialize($b); // 触发 __wakeup
?>
关键点:
• __sleep() 在 serialize() 时调用
• __wakeup() 在 unserialize() 时调用 ← 漏洞利用点!
3. __get() 和 __set()
访问不存在或不可访问的属性时触发:
<?php
class Person {
private $str = 'hello';
function __get($name) {
echo "读取不可访问属性时调用 __get<br>";
return $this->str;
}
function __set($name, $value) {
echo "设置不可访问属性时调用 __set<br>";
$this->str = $value;
}
}
$a = new Person();
echo $a->age; // 触发 __get(age属性不存在)
$a->age = 13; // 触发 __set
?>
4. __call() 和 __callStatic()
调用不存在的方法时触发:
<?php
class Person {
function __call($method, $args) {
echo "调用不存在方法时触发 __call<br>";
}
public static function __callStatic($method, $args) {
echo "调用不存在静态方法时触发 __callStatic<br>";
}
}
$a = new Person();
$a->method(); // 触发 __call
Person::method2(); // 触发 __callStatic
?>
5. __toString()
对象被当字符串使用时触发:
<?php
class Person {
public $name = 'derry';
function __toString() {
return $this->name;
}
}
$a = new Person();
echo $a; // 触发 __toString,输出: derry
?>
6. __invoke()
对象被当函数调用时触发:
<?php
class Person {
function __invoke($p1, $p2) {
return $p1 . "---" . $p2;
}
}
$a = new Person();
echo $a("hello", "world"); // 触发 __invoke,输出: hello---world
?>
7. __isset() 和 __unset()
对不可访问属性调用 isset() 或 unset() 时触发:
<?php
class Person {
private $name;
function __isset($name) {
echo "调用 __isset<br>";
}
function __unset($name) {
echo "调用 __unset<br>";
}
}
$person = new Person();
isset($person->name); // 触发 __isset
unset($person->name); // 触发 __unset
?>
六、魔术方法速查表
| 魔术方法 | 触发时机 | 常见用途 |
|---|---|---|
__construct() |
创建对象时 | 初始化 |
__destruct() |
销毁对象时 | 清理资源 |
__sleep() |
serialize() 前 |
指定要序列化的属性 |
__wakeup() |
unserialize() 后 |
反序列化漏洞核心! |
__get() |
读取不可访问属性时 | 动态属性访问 |
__set() |
设置不可访问属性时 | 动态属性设置 |
__call() |
调用不存在方法时 | 方法拦截 |
__callStatic() |
调用不存在静态方法时 | 静态方法拦截 |
__toString() |
对象转字符串时 | 输出格式化 |
__invoke() |
对象当函数调用时 | 闭包模拟 |
__clone() |
clone 克隆对象时 |
深拷贝 |
__isset() |
对不可访问属性 isset() 时 |
属性检测 |
__unset() |
对不可访问属性 unset() 时 |
属性删除 |
七、反序列化漏洞原理
漏洞本质 :用户可控的字符串被 unserialize() 反序列化,触发魔术方法执行恶意代码!
典型攻击链:
用户输入恶意序列化字符串
↓
unserialize() 反序列化
↓
触发 __wakeup() 或 __destruct()
↓
执行恶意代码(读文件、RCE等)
八、防御方法
| 防御措施 | 具体做法 |
|---|---|
| 禁用反序列化 | 不使用 unserialize(),改用 json_decode() |
| 白名单校验 | 只允许特定类被反序列化 |
| 签名验证 | 序列化数据加签名,反序列化前验证 |
| 过滤危险类 | 检查序列化字符串中的类名 |
PHP配置:
; 禁用反序列化(极端做法)
disable_functions = unserialize
安全替代方案:
// 不安全
$obj = unserialize($data);
// 安全:使用 JSON
$obj = json_decode($data, true);
总结
序列化漏洞的核心:
1. 用户能控制序列化字符串的内容
2. unserialize() 会触发魔术方法
3. 魔术方法里可能执行危险操作
记住一句话 :永远不要对用户输入的数据直接调用 unserialize()!