抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

反序列化

首先,反序列化(unserialize)对应序列化(serialize),通俗理解就是后者将对象转化为字符串,前者将字符串转化为对象,但是由于前者的可操作性带来了危害。

php反序列化漏洞主要操作的是对象,也可以称为php对象注入漏洞,当调用unserialize函数对一个对象进行反序列时,如果该对象内部使用了魔法函数,并且魔法函数中包含了一些危险可控的操作,那么攻击者可能利用这点构造一段恶意代码执行,这就是反序列化漏洞。

php中提供了一些魔法函数,这些函数可以直接使用,不需要声明,常用的魔法函数如下:

方法名 调用条件
__call 调用不可访问或不存在的方法时被调用
__callStatic 调用不可访问或不存在的静态方法时被调用
__clone 进行对象clone时被调用,用来调整对象的克隆行为
__constuct 构建对象的时被调用;(但在unserialize()时是不会自动调用的)。
__debuginfo 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__destruct 明确销毁对象或脚本结束时被调用;
__get 读取不可访问或不存在属性时被调用
__invoke 当以函数方式调用对象时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
__set 当给不可访问或不存在属性赋值时被调用
__set_state 当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
__sleep 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
__toString 当一个类被转换成字符串时被调用
__unset 对不可访问或不存在的属性进行unset时被调用
__wakeup 当使用unserialize时被调用,可用于做些对象的初始化操作

这里先以_toString()函数为例:

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 Test1{
public $file_name;
//当对象当做字符串使用会被自动调用
function __toString(){
return file_get_contents($this->file_name);
}
}
$test1 = new Test1();
//序列化
echo serialize($test1);
class Test2{
public function test2_unserialize(){
//反序列化test1对象
//file_name文件即test.txt
$test1_str = 'O:5:"Test1":1:{s:9:"file_name";s:8:"test.txt";}';
return unserialize($test1_str);
}
$test2 = new Test2();
$obj = $test2->test2_unserialize();
echo "============";
//输出test1对象,会自动调用__toString方法
echo $obj;
?>

我来解说下,同时也是传达思想。test1对象内部使用了_toString()魔法函数,并且在_toString()函数内部使用了file_get_contents函数读取了属性$file_name指向文件,但_toString函数只有在test1对象当做字符串使用的情况下才会被调用。Test2对象中调用了test2_unserialize函数对test对象进行反序列化,并将test1对象输出,那么此时就会调用test1对象里的_toString函数读取文件。

test.txt文件里的内容就会被输出。假设$test1_str变量里的内容如果是可控的话,那么就可以将test.txt内容进行替换,进行任意文件读取。这时,就有一个反序列化pop利用链。

2.反序列化pop利用链

pop漏洞利用链代码审计:pop(面向对象编程)链的构造是寻找程序当中环境已经定义了或者能够动态加载的对象属性(函数方法),将一些可能的调用组合在一起形成一个完整的,具有目的性的操作。

反序列化漏洞的挖掘可以从以下方面进行:

  1. 进行反序列化的数据点可控

  2. 反序列化类中有魔法方法,并且魔法方法中有敏感操作

  3. 魔法方法中没有敏感操作,但是其对象调用了其它类中同名函数,可以通过构造pop链利用

unserialize函数(变量可控)–>__wakeup()魔术方法–>__tostring()魔术方法–>__get魔术方法–>__invoke魔术方法–>触发Read类中的file_get方法–>触发file_get_contents函数读取flag.php

反序列化的常见起点
    __wakeup 一定会调用
    __destruct 一定会调用
    __toString 当一个对象被反序列化后又被当做字符串使用

反序列化的常见中间跳板:
    __toString 当一个对象被当做字符串使用
    __get 读取不可访问或不存在属性时被调用
    __set 当给不可访问或不存在属性赋值时被调用
    __isset 对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();

反序列化的常见终点:
    __call 调用不可访问或不存在的方法时被调用
    call_user_func 一般php代码执行都会选择这里
    call_user_func_array 一般php代码执行都会选择这里

主要还是三点:

起点
跳板
代码执行

这里可以了解到,反序列化unserialize会调用__wakeup()函数进行对象初始化,我们就要想办法绕过此函数,这里有一个点当传入对象大于类的对象时可以绕过weakup函数,但是这对php版本是有要求的。PHP2-php5.6.25和php7-php7.0.10这两个区间内可行。

文件的输出与输入

先介绍序列化serialize,对象转化为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes'; //可以看到有两个对象
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
public function __wakeup(){
$this->username = "guests";
}
public function fun(){
echo $this->username;echo "<br>";echo $this->password;
}
}
$a = serialize(new Name("admin",200));
echo $a;
?>

O:4: 这是一个object对象,长度为4
“Name”:2: 代表名字为Name,Name有两个属性
{
s:14:“Nameusername”; string类型,属性名长度为14,属性名是username //这里为什么是14,为什么属性名是username而不是Nameusername后面会说
s:5:“admin”; string类型,参数值长度5,参数值是admin
s:14:“Namepassword”; string类型,属性名长度为14,属性名是password
i: 100; int类型,参数值是100
}

这时序列化的输出,反序列化我们只需要按照格式创建自己需要的字符串就ok。

再扩充下php序列化基础格式

boolean

b:;

b:1; //True

b:0; //False

integer

i:;

i:1; // 1

i:-3; // -3

double

d:;

d:1.2345600000000001; // 1.23456(php弱类型所造成的四舍五入现象)

NULL

N; //NULL

string

s::””; //前面已经有介绍

array

a::{key,value pairs};

a{s”key1”;s”value1”;s”value2”;} // array(“key1” => “value1”, “key2” => “value2”)

关于对象

说起对象,emm,看代码,先要了解publicprivateprotected这三种。

其中public正常使用就行,不用添加也不用减少

protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的 \0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0 组合。这也许解释了,为什么如果直接在网址上,传递\0*\0username会报错,因为实际上并不是\0,只是用它来代替ASCII值为0的字符。必须用python传值才可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Name{
protected $username = 'nonono';
protected $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
public function __wakeup(){
$this->username = "guests";
}
public function fun(){
echo $this->username;echo "<br>";echo $this->password;
}
}
$a = serialize(new Name("admin",100));
echo $a;
?>
O:4:"Name":2:{s:11:"\0*\0username";s:5:"admin";s:11:"\0*\0password";i:100;}
?>

private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上\0的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
public function __wakeup(){
$this->username = "guests";
}
public function fun(){
echo $this->username;echo "<br>";echo $this->password;
}
}
$a = serialize(new Name("admin",100));
echo $a;
?>
O:4:"Name":2:{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}
?>

这里的\0也可以写为%00,这是空的意思,因为输出的时候不会输出空。

注意:这里的双引号有必要时需要url编码,论环境而定。

评论