序列化和反序列化的概念

序列化

序列化指将对象或者数组转化为可以存储的字符串

在PHP中使用serialize()函数来将对象或者数组进行序列化,并返回一个包含字节流的字符串来表示。

比如我在网上找到了别人举的一个例子和讲解(这篇博客后面的内容大部分也都是搬运性质)

PHP反序列化研究

要注意保护和私有的成员变量

如保护的是%00*%00成员名;……

私有的是%00类名%00成员名;……

这里表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。

同时,要注意到serialize()函数只对类的属性序列化,不序列化方法。

序列化方便传输和存储,所以开发中会使用

反序列化

反序列化指将序列化后的字符串转化回对象或者数组

在PHP中使用unserialize()函数来将序列化后的字符串转换回PHP的值,并返回的值可为 integer、float、string、array 或 object类型。

同样,看那个文章可以看到例子和讲解

反序列化漏洞的产生

反序列化漏洞是由于unserialize函数接收到了恶意的序列化数据篡改成员属性后导致的。

PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,是因为程序对输入数据处理不当导致的. 反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控。函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。

php魔术方法

PHP中将双下划线__保留为魔术方法,所有的魔术方法必须声明为 public。

__construct()

**__construct()**被称为构造方法,也就是在创造一个对象时候,首先会去执行的一个方法。但是在序列化和反序列化过程是不会触发的。

类似于c++里的构造函数

__destruct()

在到某个对象的所有引用都被删除或者当对象被显式销毁时执行的魔术方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class User{

public function __destruct()
{
echo "__destruct test</br>";
}

}
$test = new User();
$ser = serialize($test);
unserialize($ser);
?>

运行结果:

1
2
__destruct test
__destruct test

可以看到执行了两次**__destruct**,因为一个就是实例化的时候创建的对象,另一个就是反序列化后生成的对象。

__call

在对象中调用一个不可访问方法时,**__call()** 会被调用。也就是说你调用了一个对象中不存在的方法,就会触发。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User{

public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}

}
$test = new User();
$test->callxxx('a');
?>

运行结果:

1
callxxx,a

可以看到__call需要定义两个参数,一个是表示调用的函数名,一般开发会在这里报错写xxx不存在这个函数,第二个参数是传入的数组,这里只传入了一个a。

__callStatic

静态上下文中调用一个不可访问方法时,**__callStatic()** 会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User{

public static function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}

}
$test = new User();
$test::callxxx('a');
?>

运行结果:

1
callxxx,a

__get

读取不可访问属性的值时,__get()会被调用

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User{
public $var1;
public function __get($arg1)
{
echo $arg1;
}

}
$test = new User();
$test->var2;
?>

运行结果:

1
var2

__get方法需要一个参数,这个参数代表着访问不存在的属性值

__set

给不可访问属性赋值时,__set()会被调用

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User{
public $var1;
public function __set($arg1,$arg2)
{
echo $arg1.','.$arg2;
}

}
$test = new User();
$test->var2=1;
?>

运行结果:

1
var2,1

set跟get相反,一个是访问不存在的属性,一个是给不存在的属性赋值。

__isset

对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User{
private $var;
public function __isset($arg1)
{
echo $arg1;
}

}
$test = new User();
isset($test->var1);
?>

运行结果:

1
var1

该魔术方法使用了isset()或者empty()只要属性是private或者不存在的都会触发。

__unset

对不可访问属性调用 unset() 时,__unset() 会被调用。

1
2
3
4
5
6
7
8
9
10
11
<?php
class User{
public function __unset($arg1)
{
echo $arg1;
}

}
$test = new User();
unset($test->var1);
?>

运行结果:

1
var1

如果一个类定义了魔术方法 __unset() ,那么我们就可以使用 unset() 函数来销毁类的私有的属性,或在销毁一个不存在的属性时得到通知。

__sleep

serialize() 函数会检查类中是否存在一个魔术方法 **__sleep()**。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。

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
<?php
class User{
const SITE = 'uusama';

public $username;
public $nickname;
private $password;

public function __construct($username, $nickname, $password)
{
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}

// 重载序列化调用的方法
public function __sleep()
{
// 返回需要序列化的变量名,过滤掉password变量
return array('username', 'nickname');
}

}
$user = new User('a', 'b', 'c');
echo serialize($user);

运行结果:

1
O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}

可以看到执行序列化之前会先执行sleep()函数,上面sleep的函数作用是过滤掉password的变量值。

__wakeup

unserialize() 会检查是否存在一个__wakeup()方法。如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源。

预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class User{
const SITE = 'uusama';

public $username;
public $nickname;
private $password;
private $order;

public function __construct($username, $nickname, $password)
{
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}

// 定义反序列化后调用的方法
public function __wakeup()
{
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));

运行结果:

1
2
3
4
5
6
7
8
9
10
class User#1 (4) {
public $username =>
string(1) "a"
public $nickname =>
string(1) "b"
private $password =>
string(1) "a"
private $order =>
NULL
}

可以看到执行反序列化之前会先执行wakeup()函数,上面wakeup的函数作用是将username的变量值赋值给password变量。

__toString

__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User{

public function __toString()
{
return '__toString test';
}

}

$test = new User();
echo $test;

运行结果:

1
__toString

特别注意__toString的触发条件,引用k0rz3n师傅的笔记:

(1)echo ($obj) / print($obj) 打印时会触发 (2)反序列化对象与字符串连接时 (3)反序列化对象参与格式化字符串时 (4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型) (5)反序列化对象参与格式化SQL语句,绑定参数时 (6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时 (7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用 (8)反序列化的对象作为 class_exists() 的参数的时候

__invoke

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。(本特性只在 PHP 5.1.0 及以上版本有效。)

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User{

public function __invoke()
{
echo '__invoke test';
}

}

$test = new User();
$test();

运行结果:

1
__invoke test

__clone

当使用 clone 关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法 __clone() ,如果该魔术方法存在的话。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User{

public function __clone()
{
echo "__clone test";
}

}
$test = new User();
$newclass = clone($test);
?>

运行结果:

1
__clone test