前言

以前一直看 K0r3zn 师傅的一些文章,最近终于去加了师傅的好友 : )。师傅说学习要有一些知识的产出,那我就把前段时间学的知识整理一下吧~

序列化与反序列化

在 php 中,常见的序列化就是使用serialize()将一个对象中的信息用字符串的形式记录。因为对象是在内存中存储的,其会随着脚本结束而销毁。但有些情况下需要将对象当前的状态保存下来,比如其中含有一些之后可能用到的属性。对象序列化可以将对象的状态通过数值和字符记录下来,便于存储和传输。在需要的时候将对象反序列化恢复后使用。序列化的对象可以是classarray等。
反序列化就是使用unseralize()将序列化后的字符串,还原成相应的对象,即序列化的逆过程。

对象的属性初始化

当给一个对象的属性初始化时,可以给其属性赋予numberstringarraynullbool类型的值。

1
2
3
4
5
6
7
8
9
10
class test {
public $num = 18;
public $str = 'll';
public $arr = array('ll' => 20, 'K' => 'person');
public $n = null;
public $b = true;
}
$a = new test();
echo serialize($a);
// O:4:"test":5:{s:3:"num";i:18;s:3:"str";s:2:"ll";s:3:"arr";a:2:{s:2:"ll";i:20;s:1:"K";s:6:"person";}s:1:"n";N;s:1:"b";b:1;}

如果在属性声明的时候赋予object类型,则会报错。但是可以在该对象的方法中为属性赋予object类型的值。

1
2
3
4
5
6
7
8
9
10
11
class LL {}
class test1 {
public $obj = new LL();
}
// PHP Fatal error: Constant expression contains invalid operations
class test2 {
public $obj;
function __construct() {
$this->obj = new LL();
}
}

序列化格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class LL {
public $name = 'hanhan';
}
class test {
public $num = 18;
public $str = 'll';
public $arr = array('ll' => 20, 'K' => 'person');
public $n = null;
public $b = true;
public $obj;
function __construct() {
$this->obj = new LL();
}
}
$a = new test();
echo serialize($a);
// $a = serialize($a);
// var_dump(unserialize($a));

这里所有属性都定义为 public 类型,对象序列化的大致格式如下:
O:类名长度:"类名":对象的属性个数:{属性1;属性2;属性3;...}

01

属性1、属性2…等属性的具体格式如下:

1
2
3
4
5
6
number类型:s:属性名的长度:"属性名";i:属性值
string类型:s:属性名的长度:"属性名";s:属性值的长度:属性值
array类型:s:属性名的长度:"属性名";a:数组长度:{元素a,元素b...}
null类型:s:属性名的长度:"属性名";N
bool类型:s:属性名的长度:"属性名";b:1 // 如果是false则以b:0结尾,b代表bool
object类型:s:属性名的长度:"属性名";O:类名长度:"类名":对象的属性个数:{属性1;属性2;...}

其中array类型和object类型的属性具体表示与上述格式一致。
注意:序列化对象时,不会保存常量的值,但是会保留父类中的属性。

访问修饰符

publicprivateprotected
public(公有):公有属性和方法可以在类的内部和外部访问。
private(私有):私有属性和方法只能在其被定义的类内部访问,不会被继承。
protected(受保护):受保护的属性和方法只能在内部访问,存在于任何子类中,可以被其自身以及其子类和父类访问。

前面使用的属性都是被定义为public,其序列化的格式如上。而被定义为privateprotected的属性值在属性名处有所区别,其他部分同public

1
2
3
4
5
6
7
8
9
class LL {}
class test {
private $name = 'll';
protected $age = 12;
}
$a = new test();
echo serialize($a); // O:4:"test":2:{s:10:"testname";s:2:"ll";s:6:"*age";i:12;}
$data = serialize($a);
file_put_contents("test.txt", $data);

可以看到private属性名name变成了testname,而且该属性名的长度不是8,而是10。protected属性名age变成了*age,长度不是4,而是6。
实际上,private属性名的格式为%00类名%00属性名,即%00test%00name,而protected属性名的格式为%00*%00属性名,即%00*%00name。它们都增加了两个空字符%00,所以长度和实际看到的相差了2。

02

我觉得可以这样记:
因为private属性只能在其被定义的类内部访问,且不会被继承,所以要在该属性前加上其类名,告诉我们它是私有的。而protected属性可以在其父类和子类中访问,所以在其前面加上*,告诉我们它是受保护的。最后不要忘记加上两个空字符%00

php序列化只会对属性进行操作,不会对方法进行序列化。因为平常我们可能只需要在1.php中记录一个对象的状态,而在2.php中也有该类的定义,那我们便可以对1.php中的对象进行序列化,到2.php中进行反序列化取出该状态(属性值)去做后续处理。也可以将1.php中的实例化的对象长久地保存在磁盘等设备上。

反序列化格式

1
2
3
4
5
6
7
8
9
10
11
12
class test {
public $name = 'll';
}
$a = new test();
$a = serialize($a);
var_dump(unserialize($a));
/*
object(test)#1 (1) {
["name"]=>
string(2) "ll"
}
*/

php反序列化漏洞

php 反序列化漏洞,又称 php 对象注入漏洞。因为序列化对象时,只会存储其属性,所以该漏洞可利用的一个基本条件是对象中的属性可控,除此之外,我们可能还需要一些函数的帮助来触发该漏洞。

魔术方法

php中将所有以__开头的类方法保留为魔术方法,它们都有一些特殊的用法。
常见的有:
__construct()__destruct()__call()__callStatic()__set()__get()__isset()__unset()__sleep()__wakeup()__toString()__invoke()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__construct([ mixed $args = "" [, $...]]) : void
构造函数。具有构造函数的类会在每次创建新对象时调用此方法,初始化对象。

__destruct( void ) : void
析构函数。会在某个对象的所有引用都被删除或者当对象被显示销毁时执行。

public __sleep( void ) : array
serialize()会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果该方法未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误。
__sleep()不能返回父类的私有成员的名字。这样会产生一个E_NOTICE级别的错误。

__wakeup( void ) : void
unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup()方法,预先准备对象需要的资源。
__wakeup()经常用于反序列化,例如重新建立数据库连接,或执行其他初始化操作。

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

__invoke([ $... ]) : mixed
当尝试以调用函数的方式调用一个对象时,__invoke()会被自动调用。

这里要注意toString()触发的条件,当该对象被传入任何接收string类型参数的函数中时都会触发,且在将对象与字符串进行连接和比较等操作时也会触发。

overloading
动态地“创建”类属性和方法。通过魔术方法来实现。
当调用当前环境下未定义或不可见得类属性或方法时,重载方法会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public __call( string $name, array $arguments ) : mixed
在对象中调用一个不可访问的方法时,__call()会被调用。
$name:要调用的不存在的方法名称,$arguments:一个枚举数组,包含着要传递给方法$name的参数。

public static __callStatic( string $name, array $arguments ) : mixed
在静态上下文中调用一个不可访问的方法时,__callStatic()会被调用。
$name:要调用的不存在的方法名称,$arguments:一个枚举数组,包含着要传递给方法$name的参数。

属性重载只能在对象中进行。在静态方法中,这些魔术方法将不会被调用,所以这些方法都不能被声明为static。
public __set( string $name, mixed $value ) : void
在给不可访问属性赋值时,__set()会被调用。

public __get( string $name ) : mixed
读取不可访问属性的值时,__get()会被调用。

public __isset( string $name ) : bool
当对不可访问属性调用isset()和unset()时,__isset()会被调用。

public __unset( string $name ) : void
当对不可访问属性调用unset()时,__unset()会被调用。

为什么说需要这些魔术方法呢?因为我们只是单纯地控制某一个变量的属性并不能造成什么危害,而当我们通过这些魔术方法当作跳板,去调用其他类中的一些危险的存在利用点的方法时,就会产生更大的危害。

例题

以 plaidctf-2014 中的 kpop 题目为例。实战一下,通过魔术方法去调用其他类中的方法达到写 shell 的目的。
下面是classes.php文件中我们主要利用的类:

03

这题就不细讲了。我们可以看到从Lyrics类中的魔术方法__destruct()开始。会一路调用能过滤数据、写数据到日志的其他类。但是最后写入数据的logs目录,我们没有访问权限,而路径是由LogWriter_File类中的$filename属性控制,且过滤数据的规则是由OutputFilter类的属性控制,根据前面可知,只要我们能找到可控的利用点,传入我们序列化后的数据,我们就可以自由的改变类中属性的值。
仔细读一下代码,看懂了整个逻辑后,可以发现在import.php中存在反序列化操作:

1
$data = unserialize(base64_decode($_POST['data']));

所以整体思路就是,我们序列化恶意的数据,其中控制路径和过滤的类属性根据我们需要修改,传入import.php中,脚本结束后调用__destruct()方法产生漏洞。
我们可以简单复现一下。就只写出需要用到的类和方法,并把import.php中的入口写在同一文件。

04

接下来是构造 payload 的脚本:

05

06

将该值上传,便可在相应的目录下看到 shell 文件:

07

为了更好的理解整个过程,我是通过一个一个类去调用。其实这题因为Song类中和log类中有同名的方法log(),而Lyrics类的__destruct()调用了log()方法,所以我们可以省去构造Song类这一步。这也是反序列化中需要注意的,我们可以通过同名方法实现调用其他类中的方法从而构造pop链。 不构造Song类可以参考:
https://mochazz.github.io/2019/01/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8B%E5%B8%B8%E8%A7%81%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95/

__wakeup()绕过

适用于php 5.x < php 5.6.25php 7.x < php 7.0.10。 当序列化后的字符串中表示对象属性个数的值大于真实的属性个数时,在对该字符串进行反序列化的时候就会跳过__wakeup()的执行。

1
2
3
4
5
6
7
8
9
class Test {
public $name = "ll";
public __wakeup() {
echo "hello";
}
}
// echo serialize(new Test()); // O:4:"Test":1:{s:4:"name";s:2:"ll";}
$test = 'O:4:"Test":2:{s:4:"name";s:2:"ll";}'; // 对象属性个数1改为了2
unserialize($b); // 未输出hello

unserialize()反序列化之前,会先调用__wakeup()方法。我们可以通过该技巧绕过__wakeup()中的属性检查或属性初始化。

session反序列化

session_start()被调用或者 php.ini 中session.auto_start被设置为 1 时,php 内部会调用会话管理器,访问用户的$_SESSION,并将其中的值序列化后存入指定目录的文件中。
php.ini 中有关参数:

1
2
3
4
5
6
session.auto_start  指定会话模块是否在请求开始时启动一个会话。默认为0不启动。
session.save_handler session保存形式,默认为files。
session.save_path session保存路径,默认为/tmp。文件默认以sess_xxx开头。
session.upload_progress.cleanup 一旦读取了所有post数据,立即清除进度信息。默认开启。
session.upload_progress.enabled 将上传文件的进度信息存在session中。默认开启。
session.serialize_handler 定义用来序列化和反序列化的处理器名字。默认是php。

三种处理器

我们从参数session.serialize_handler开始:

08

主要看phpphp_serialize这两种序列化机制:

1
2
3
ini_set('session.serialize_handler', '...') // php | php_serialize
sesssion_start(); // session_start('serialize_handler', '...');
$_SESSION['name'] = 'LL';

当 session.serialize_handler=php 时,/tmp 目录下的 session 文件内容为:name|s:2:"LL";
当 session.serialize_handler=php_serialize 时,session 文件内容为:a:1:{s:4:"name";s:2:"LL";}
可以看到 php_serilaize 机制就是直接对数组 $_SESSION 调用了 serialize() 。而 php 机制格式是键名|反序列化的键值

产生原因

session 反序列化漏洞产生的原因主要是因为,将信息存入 session 文件时和将信息从 session 文件中取出来时,使用了不同的 session 处理器。
仔细看一下 php 处理器的格式,会发现它是通过|分割数组键和值的,即当要从 session 文件中取出信息时,它会先去找|分割符,找到后,将前面的部分直接取出作为键名,后面的部分调用 serialize() 反序列化后作为键值放入$_SESSION数组中。
那如果我们向 session 文件中存入数据时,使用的是 php_serialize 处理机制,比如存入$_SESSION['name']='|evil',可见我恶意加了一个|分割符,存进去后,session 文件内容为a:1:{s:4:"name";s:5:"|evil";},因为在调用 serialize() 进行序列化的时候,会将键值直接放入序列化后的字符串中的某一部分,可见|evil被直接写进去了。那如果我们从 session 文件中取数据时,用了 php 处理机制,根据前面所说,会取出$_SESSION['a:1:{s:4:"name";s:5:"'] = 对 evil";} 进行反序列化后的值,而|后面的数据在我们传入时可控,在取出时会对后面的值进行反序列化。

例题

1
2
3
4
5
6
7
8
9
10
error_reporting(0);
highlight_file(__FILE__);
call_user_func($_GET['a'], $_POST);
$_SESSION['name'] = $_GET['b'];
class test {
function __destruct() {
include('flag.php');
echo $flag;
}
}

这里借用了 lctf 2018 的一行代码call_user_func($_GET['a'], $_POST);。我们先生成一个序列化后的 test 类,值为O:4:"test":0:{}。 通过call_user_func($_GET['a'], $_POST)在存储 session 信息时,改变序列化 session 的处理器,默认为 php,我们改为 php_serialize 。具体可以去看一下session_start()手册内容。

09

以 php_serialize 方式存储 session 信息,记得加|

10

可以看到我们成功将序列化的 test 类存储进 session 文件中,并且是以 php_serialize 格式:

11

带着上一步服务器返回的PHPSESSID,不传入 post 数据,即等于直接调用session_start(),此时默认处理器为 php ,取出数据时以|分割,将后面部分反序列化成 test 类,当调用 __destruct() 时读出 flag :

12

上传表单

上面的情况是我们可以控制存储和取出 session 信息时,php 对 session 信息的处理方式,存储的时候以 php_serialize 机制序列化,取出的时候以 php 机制反序列化,因为格式的差异导致部分数据可控。
那如果我们无法任意控制 php 对 session 数据的处理呢?我们可以利用另外一种技巧。

session.upload_progress.enabled是一个默认开启的选项,它会将上传文件时的进度信息,填充进 $_SESSION 数组。
当一个文件上传时,如果同时 post 一个与 php.ini 中设置的session.upload_progress.name的值同名的变量,上传进度将会写入$_SESSION中。
session.upload_progress.name默认值是PHP_SESSION_UPLOAD_PROGRESS。 当 php 检测到这种 post 请求时,会在 $_SESSION 中添加一组数据,索引是session.upload_progress.prefix的值加上 post 中PHP_SESSION_UPLOAD_PROGRESS对应的值。
session.upload_progress.prefix默认值是upload_progress_。 为了方便,我们把session.upload_progress.cleanup设置为Off

13

例题

题目直接给出了源码:

14

这里看到开启了session会话,那我们就直接去看一下phpinfo()中 session 部分的设置:

15

可以看到默认环境中是php_serialize的处理器,而因为该脚本调用了ini_set()修改了配置项,所以脚本中是php处理器。
还可以看到当OowoO类调用__destruct()方法时,会执行eval()函数,而传入该函数的是OowoO类的属性$mdzz,我们只要控制了该属性便可以构造一个 webshell。
为了看到具体过程,我们在本地复现一下。在最后打印一下 $_SESSION 可以看出反序列化时的操作。

16

要提交的序列化数据:

1
2
3
4
class OowoO {
public $mdzz = "echo 'll';";
}
echo serialize(new OowoO()); // O:5:"OowoO":1:{s:4:"mdzz";s:10:"echo 'll';";}

提交表单:

17

执行了echo 'll';

18

总结一下整个过程,首先通过上面所讲的上传技巧,将上传文件的信息放进了 $_SESSION 中,该信息的一部分内容我们可以控制,然后 php 会以默认的环境中的处理器,在这里是 php_serialize ,处理该内容并存储进了 session 文件中。在从 test.html 转到 test.php 页面后,test.php 页面中因为通过ini_set()修改了 session 处理器为 php ,所以该页面以 php 反序列化的格式取出了该内容,在反序列化时将OowoO类还原,而该类中的$mdzz属性是我们可控的。
从上图的 session 文件中看到,因为在 test.php 中是以 php 格式取出该 session 文件的内容,可见键名是我们恶意构造的|前面的部分,而键值是|后面可利用的序列化值。除了在上传文件名filename中传入序列化数据,也可以在表单的name属性中传入序列化数据,因为两者都会在 session 文件出现,只要我们控制好|分割符即可。

phar反序列化

从上面可知,一般的反序列化漏洞需要找到unserialize()函数才能利用,session反序列化,可以不需要使用到该函数,但是利用条件比较苛刻(我感觉)。而在 2018 Black Hat 上的议题中讲了一种新的反序列化利用方法,利用 phar 文件会以序列化的形式存储用户自定义的meta-data这一特性,扩展了 php 反序列化漏洞的攻击面。

phar文件结构

  1. a stub
    一个识别标志,前面的内容没有限制,但必须以__HALT_COMPILER();?>结尾,为了让 phar 扩展可以识别该文件是一个 phar 文件。
  2. a manifest describing the contents
    phar 文件本身是一个归档文件,类似 java 中的 jar 文件,里面存放了各文件的权限、属性等信息。这部分还会以序列化的形式存储用户自定义的 meta-data ,这是也是该攻击手法的核心之处。

19

  1. the file contents
    具体的文件内容。
  2. [optional] a signature for verifying Phar integrity (phar file format only)
    签名,放在文件末尾。

创建phar文件

php 中内置了一个Phar类来处理相关的操作,需要将php.ini中的phar.readonly设置为Off,否则无法生成 phar 文件,且要把 phar 写入的文件夹的权限设为可写。

20

可以看到该文件中的 meta-data ,即存入的Test类,是以序列化形式存储的:

21

php 中一大部分的文件系统函数在通过phar://伪协议解析 phar 文件时,都会将 meta-data 进行反序列化,受影响函数如下:

受影响函数列表
fileatime filectime file_exists file_get_contents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fileperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable parse_ini_file
copy unlink stat readfile

测试

在另一文件phar_test.php中测试我们刚刚生成的test.phar

1
2
3
4
5
6
7
8
<?php
class Test {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar://test.phar/test.txt';
file_get_contents($filename);

成功将序列化后的 meta-data 进行反序列化:

22

利用条件

  1. phar 文件要能够上传到服务器。
  2. 有可用的魔术方法作为跳板。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

例题

可以参考这里。 该题还在a stub中伪造了gif的文件头。因为 php 只是通过文件头中的 stub 识别 phar 文件,确切说是__HALT_COMPILER();?>这段代码,对前面内容或者后缀名没有要求。

学习理解php源码

最近对 php 源码很感兴趣,因为前段时间发现在一些 ctf 比赛中,经常会从源码入手实现一些利用。比如有关 SimpleXMLELement 类的 xxe 、有关 SoapClient 类的 ssrf 等。
正在努力学习《深入理解php内核》,但是看不懂…,这里就试试结合各位师傅的文章去理解一下 phar 反序列化的底层实现。

理解php流

在 php 中,fopen()函数可以用file://打开本地文件、可以用 url 打开远程文件、也可以用phar://打开 phar 文件,打开后会返回一个句柄,而fread()fwrite()等函数能对资源句柄进行读写操作,fclose()可以关闭资源。那 php 是如何做到使用一致的 api 对不同数据源进行操作的呢?这是因为 php 引入了“流”的概念,为不同的资源提供了统一的接口。
参考该文章可知,fopen()函数会先查找路径是否以http://ftp://等协议开头,有则从注册的包装器列表中查找对应包装器,fopen()返回的流对象由包装器打开。在基础级别上,stream api 定义php_stream对象为流操作的资源。
简单来说,php 中的流就像是 unix 中的管道,通过管道,可以将我们的数据从一个地方传输到另一个地方。任何流一旦打开,也可以应用任意数量的过滤器,这些过滤器在读取/写入流时处理数据。

理解php wrapper

因为存在各种各样的可流式数据,所以包装器可以对其接口进行封装。流操作的支持和具体操作由包装器决定。同样是读取数据(fread),从文件中读和从内存中读做法不同。另外有些操作对某些流不适用。例如 http 协议支持 fread ,但不支持 fwrite 。内置的协议包装器列表参考官方文档中的Supported Protocols and Wrappers。 在 php 中默认的流封装协议是file://,具体可参考这里。 可以通过stream_get_wrappers()查看当前系统中注册的 wrapper :

23

查看源码

我们先看一下常见的fopen()函数,定义在ext/standard/file.c line 870( ext 是官方扩展目录,包括了绝大多数 php 的函数定义和实现):

24

上部分是一些变量的初始化,主要看下部分。fopen()的主要工作是获取了流对象并通过php_stream_to_zval()转化成 php 值类型( zval )返回。流对象由php_stream_open_wrapper_ex函数返回。ctags 跟一下,发现其宏定义在main/php_stream.h line 573( main 目录主要实现了 php 的基本设施):

25

_php_stream_open_wrapper_ex()main/streams/stream.c line 1984,结合该文章可以看到_php_stream_oprn_wrapper_ex()调用了php_stream_locate_url_wrapper()获取协议包装器( wrapper ),再调用相应的包装器打开资源并返回流对象。

前面说过流操作的支持和具体操作由包装器决定,实际上,流包装器会调用 php_stream 中 ops 成员的具体函数,这些函数在包装器打开流时被正确赋值。看一下_php_stream_wrapper_ops,在main/php_stream.h line 162

26

可以看到,一个 wrapper ,支持的功能有:打开文件、删除文件、重命名文件以及获取文件的 metadata 。所以可以知道文件系统函数是通过这个 stream api 进行操作的。
再看一下file_get_contents()的定义,发现其和fopen()一样,都调用了php_stream_open_wrapper_ex()函数。根据上面分析,该函数调用了php_stream_locate_url_wrapper()获取 wrapper。
phar 组件注册了phar://这个wrapper,即fopen()file_get_contents()这些函数都能通过php_stream_locate_url_wrapper()找到该 wrapper。看一下phar://这个 wrapper 的定义:

27

这些函数都会最终都会调用phar_var_unserialize()。 总结一下,以fopen()为例,其他文件系统函数同理。首先一个流包装器会调用 php_stream 中 ops 成员的具体函数,phar 组件注册了一个phar:// wrapper ,定义了 ops 中的部分函数,fopen()是通过 ops 成员提供 stream api 进行操作,且fopen()能通过php_stream_locate_url_wrapper()找到phar:// wrapper,当调用该 wrapper 的功能时,因为每个函数实现都最终调用了phar_var_unserialize(),所以所有文件系统函数都可以触发此 phar 漏洞。

这些文件系统函数的一个共同特点就是,都调用了php_stream_locate_url_wrapper()去获取 wrapper ,所以可以全局搜索一下调用该函数的php_stream_open_wrapper_ex()

整理

exif

  • exif_thumbnail
  • exif_imagetype

gd

  • imageloadfont
  • imagecreatefrom…

hash

  • hash_hmac_file
  • hash_file
  • hash_update_file
  • md5_file
  • sha1_file

file/url

  • touch()
  • get_meta_tags
  • get_headers

standard

  • getimagesize
  • getimagesizefromstring

zip

1
2
3
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');

更多请看zsx师傅、k0rz3n师傅的博客。

测试

phar://不能出现在前几个字符时候,可以使用compress.bzip2或者compress.zlib://

28

出现场景

内容来自owasp:
在应用程序中,序列化可能被用于:

  • 远程和进程间通信(rpc/ipc)
  • 连线协议、web服务、消息代理
  • 缓存/持久性
  • 数据库、缓存服务器、文件系统
  • http cookie、html表单参数、api身份验证令牌

防御方法

因为反序列化的缺陷可能导致远程代码执行等严重的攻击,所以我们需要对其进行防护:

  1. 对传入 unserilize() 的参数,进行严格地过滤。
  2. 在文件系统函数的参数可控时,进行严格地过滤。
  3. 严格检查上传文件内容,不能只是单纯地检查文件头
  4. 条件允许的情况下,禁用可执行系统命令、代码的危险函数。
  5. 注意不同类中的同名方法的编写,避免被用作反序列化的跳板。

总结

本文只是我对 php 反序列化漏洞的一点点理解与思考,也参考了很多的好文章。里面可能存在许多错误,尤其在源码学习这一部分,主要加上了很多在我看完文章分析后的个人理解,仅当作一点点参考,详细地分析可以到下面的链接中去学习 : )。

ref:
https://www.k0rz3n.com/2018/11/19/一篇文章带你深入理解PHP反序列化漏洞/#0X05-利用-phar-拓展-PHP-反序列化的攻击面
https://mochazz.github.io/2018/12/30/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96bug/
https://mochazz.github.io/2019/01/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8Bsession%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
https://paper.seebug.org/680/
https://blog.zsxsoft.com/post/38?from=timeline&isappinstalled=0
https://tlanyan.me/php-review-stream/
https://laravelacademy.org/post/7459.html%EF%BC%8C%E9%82%A3%E4%B9%88%E8%BF%99%E4%B8%AA%E8%AF%B4%E6%98%8E%E4%BA%86%E4%B8%80%E4%B8%AA%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%E5%91%A2%EF%BC%9F%E8%AF%B4%E6%98%8E%E6%88%91%E4%BB%ACPHP