1.序列化与反序列化
首先要了解序列化与反序列化的定义,以及序列化反序列化所用到的基本函数。
序列化:把对象转换为字节序列的过程称为对象的序列化,相当于游戏中的存档。
PHP 中的序列化函数serialize()
**serialize()**函数用于序列化对象或数组,并返回一个字符串。serialize()函数序列化对象后,可以很方便的将它传递给其他需要它的地方,且其类型和结构不会改变。
示例:
 <?phphighlight_file(__FILE__);$sites=array('I', 'Like', 'PHP');echo'<br/>';var_dump(serialize($sites));    //把这个对象进行序列化echo'<br/>';classman{public$name="xiaocui";public$sex="man";private$age=26;}$M=newman();//创建一个对象var_dump(serialize($M)); //把这个对象进行序列化?>
   复制代码
 
输出结果为:
 string(47) "a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}"string(79) "O:3:"man":3:{s:4:"name";s:7:"xiaocui";s:3:"sex";s:3:"man";s:8:"manage";i:26;}"
   复制代码
 
参数说明:
 数组的序列化:a 代表一数组  3 代表数组中有3个元素i 代表数组的下标0 代表I元素的下标值s 代表元素I的数据类型为字符型1 代表元素I的长度为1对象的序列化:O 代表是一个对象3 代表类名man的长度3 代表类中的字段数s 代表属性name的类型为字符型4 代表属性name的长度
//后面的以此类推,序列化字符串中字段内容以{开始,;}结束
   复制代码
 
反序列化:把字节序列恢复为对象的过程称为对象的反序列化,相当于游戏中的读档。
【一一帮助安全学习,所有资源获取处一一】
①网络安全学习路线
②20 份渗透测试电子书
③安全攻防 357 页笔记
④50 份安全攻防面试指南
⑤安全红队渗透工具包
⑥信息收集 80 条搜索语法
⑦100 个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年 CTF 夺旗赛题解析
PHP 中的反序列化函数unserialize()
**unserialize()**函数用于将序列化后的字符串在还原为原来的数组或对象的过程。**unserialize()**函数可以快速将序列化的字符串还原出来,用来完成它本来的功能。
示例代码:
 <?phphighlight_file(__FILE__);$sites=array('I', 'Like', 'PHP');echo'<br/>';echo$ser=serialize($sites).'<br/>';    //把这个对象进行序列化var_dump(unserialize($ser)); //把序列化的字符串进行反序列化echo'<br/>';classman{public$name="xiaocui";public$sex="man";private$age=26;}$M=newman();//创建一个对象echo$ser=serialize($M).'<br/>'; //把这个对象进行序列化var_dump(unserialize($ser));   //把序列化的字符串进行反序列化?>
   复制代码
 
输出结果为:
 a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}array(3) { [0]=>string(1) "I"[1]=>string(4) "Like"[2]=>string(3) "PHP"}O:3:"man":3:{s:4:"name";s:7:"xiaocui";s:3:"sex";s:3:"man";s:8:"manage";i:26;}object(man)#2 (3) { ["name"]=> string(7) "xiaocui" ["sex"]=> string(3) "man" ["age":"man":private]=> int(26) } 
   复制代码
 
可以看出上述反序列化后的数组或对象,与原数据没有任何变化。
序列化与反序列化在系统中的作用
①把对象的字节序列永久放在磁盘中,需要时可以随时调用,大大节省磁盘占用空间。
②在传输过程中可以直接传输字节序列,而不是对象,这可以大大提高传输速率。
在业务系统中,需要对一些对象进行序列化存储,让他们离开内存空间,存放在一个文件中,以便持久化保存。例如:在用户注册并登录系统时,会将用户信息如用户名,密码,cookie 等信息先通过序列化存储起来,当用户再次登录时将序列化后的字节序列通过反序列化成原对象到内存进行使用,可以大大节省内存开销。
2.魔术方法
PHP 将所有以__(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以__为前缀。
PHP 中常见魔术方法
__construct()
具有 __construct 函数的类会在每次创建新对象时先调用此方法,适合在使用对象之前做一些初始化工作。
代码示例:
 <?phphighlight_file(__FILE__);classdemo{public$name="xiaocui";public$sex="man";private$age=26;publicfunction__construct(){echo"<br/>"."类被实例化时调用我!";}}$D=newdemo();   //实例化对象?>
   复制代码
 
输出结果:
__destruct()
该函数会在到某个对象的所有引用都被删除或者当对象被销毁时执行
代码示例:
 <?phphighlight_file(__FILE__);class demo{public $name="xiaocui";public $sex="man";private $age=26;public function __construct(){echo "<br/>"."类被实例化时调用我!"."<br/>";}public function num($a,$b){echo $c = $a + $b.'<br/>';return $c;}public function __destruct(){echo "当类中的所有方法都被销毁时调用我!";}public function person($per){echo "We are $per !!!".'<br/>';}}$D=new demo();   //实例化对象$D->num(5,6);   //调用num()方法$D->person(man);  //调用person()方法?>
   复制代码
 
输出结果:
 类被实例化时调用我!11Weareman!!!当类中的所有方法都被销毁时调用我!
   复制代码
 
上述输出结果可以很直观的看到代码的执行顺序:__construct()=>num(5,6)=>person(nanren)=>__destruct()
实例化对象时首先要执行构造方法__construct(),然后接着执行类中的实例num()、person(),当所有方法都执行完成销毁时,最后调用析构方法__destruct()。
__wakeup()
在使用unserialize()时,会检查是否存在一个__wakeup()魔术方法。如果存在,则该方法会先被调用,预先准备对象需要的资源。
代码示例:
 <?phphighlight_file(__FILE__);classdemo{public$name="xiaocui";protected$sex="man";private$age=26;publicfunction__construct(){echo"<br/>"."类被实例化时调用我!"."<br/>";}publicfunction__destruct(){echo"<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";}publicfunction__wakeup(){echo"<br/>"."当反序列时首先调用我 !".'<br/>';}}$D=newdemo();   //实例化对象echo$ser=serialize($D);      //序列化对象$Dvar_dump(unserialize($ser));    //反序列化字符串$ser?>
   复制代码
 
输出结果:
由于$age为私有属性,在序列化时会在前后加空白字符 %00,原本的字符长度为 7,而在两侧加入空白字符则字符长度就会变为 9
 类被实例化时调用我!O:4:"demo":3:{s:4:"name";s:7:"xiaocui";s:6:"*sex";s:3:"man";s:9:"demoage";i:26;}当反序列时调用我 !object(demo)#2 (3) { ["name"]=> string(7) "xiaocui" ["sex":protected]=> string(3) "man" ["age":"demo":private]=> int(26) }当类中的所有方法都被销毁时调用我!当类中的所有方法都被销毁时调用我!
   复制代码
 
上述输出结果可以很直观的看到代码的执行顺序:__construct()=>serialize($D)=>__wakeup()=>unserialize($ser)=>__destruct()=>__destruct()
实例化对象时首先要执行构造方法__construct(),然后执行序列化serialize($D),然后再反序列化之前要执行__wakeup()方法,然后执行反序列化unserialize($ser),当所有方法都执行完成销毁时最后执行__destruct()析构方法,由于反序列化后将原来的序列化字符串还原又执行一次__destruct()。
__toString()
__toString()方法用于定义一个类被当成字符串时该如何处理。
示例代码:
 <?phphighlight_file(__FILE__);class demo{    public $name="xiaocui";    protected $sex="man";    private $age=26;    public function __construct()    {        echo "<br/>"."类被实例化时调用我!"."<br/>";    }    public function __destruct(){        echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";    }    public function __wakeup()    {        echo "<br/>"."当反序列时调用我 !".'<br/>';    }    public function __toString(){        return "<br/>"."类被当成字符串处理时调用我!"."<br/>";    }}$D=new demo();   //实例化对象echo $D;   //类被当成字符串输出
?>
   复制代码
 
输出结果:
 类被实例化时调用我!类被当成字符串处理时调用我!当类中的所有方法都被销毁时调用我!
   复制代码
 
上述输出结果可以看到,当类被当成字符串echo时,__toString()方法被调用并执行。
如果该类中没有__toString()方法,进行 echo 输出时,PHP 会抛出致命性错误,错误如下:
 Catchablefatalerror: ObjectofclassdemocouldnotbeconvertedtostringinD:\XXXX\phpstudy_pro\WWW\two\demo.phponline30
   复制代码
 __sleep()
在使用serialize()函数时,程序会检查类中是否存在一个__sleep()魔术方法。如果存在,则该方法会先被调用,然后再执行序列化操作。
__sleep()方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,该函数就起到了很好的清理作用。
示例代码:
 <?phphighlight_file(__FILE__);class demo{    public $name="xiaocui";    protected $sex="man";    private $age=26;    public function __construct()    {        echo "<br/>"."类被实例化时调用我!"."<br/>";    }    public function __destruct(){        echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";    }    public function __wakeup()    {        echo "<br/>"."当反序列时调用我 !".'<br/>';    }    public function __sleep(){        echo "<br/>"."当序列时调用我 !".'<br/>';        return array("name","sex","age");    //这里必须返回一个数值,里边的元素表示返回的属性名称    }
}$D=new demo();   //实例化对象
echo $ser = serialize($D);  //序列化对象
?>
   复制代码
 
输出结果:
 类被实例化时调用我!当序列时调用我!O:4:"demo":3:{s:4:"name";s:7:"xiaocui";s:6:"*sex";s:3:"man";s:9:"demoage";i:26;}当类中的所有方法都被销毁时调用我!
   复制代码
 
上述输出结果可以看到,当类被序列化时首先调用了__sleep()方法,该函数必须返回一个数值。如果该函数没有返回属性的话,序列化时会将属性清空。
__invoke()
当尝试以调用函数的方式调用一个对象时,__invoke方法会被自动调用。(本特性只在 PHP 5.3.0 及以上版本有效。)
代码示例:
 <?phphighlight_file(__FILE__);class demo{    public $name="xiaocui";    protected $sex="man";    private $age=26;    public function __construct()    {        echo "<br/>"."类被实例化时调用我!"."<br/>";    }    public function __destruct(){        echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";    }    public function __wakeup()    {        echo "<br/>"."当反序列化时调用我 !".'<br/>';    }    public function __sleep(){        echo "<br/>"."当序列化时调用我 !".'<br/>';        return array("name","sex","age");
    }    public function __invoke()    {    echo "<br/>"."当以函数的方式调用对象时会调用我!".'<br/>';    }
}$D=new demo();   //实例化对象
$D();    //以函数的方式调用对象?>
   复制代码
 
结果输出:
 类被实例化时调用我!当以函数的方式调用对象时会调用我!当类中的所有方法都被销毁时调用我!
   复制代码
 
上述结果可以看到当使用函数的方法调用对象时,就会调用__invoke()方法.
如果没有类中没有该方法,那么 PHP 会报出致命性错误,如下:
 Fatalerror: UncaughtError: FunctionnamemustbeastringinD:\xxxxx\phpstudy_pro\WWW\two\demo.php:42Stacktrace: #0 {main} thrown in D:\xxxxx\phpstudy_pro\WWW\two\demo.php on line 42
   复制代码
 __call()
在对象中调用一个不存在或者不可访问方法时,__call会被调用。
代码示例:
 <?phphighlight_file(__FILE__);class demo{    public $name="xiaocui";    protected $sex="man";    private $age=26;    public function __construct()    {        echo "<br/>"."类被实例化时调用我!"."<br/>";    }    public function num($a,$b){        echo "<br/>".$c = $a + $b.'<br/>';        return $c;    }    public function __destruct(){        echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";    }    public function person($per){        echo "<br/>"."We are $per !!!".'<br/>';    }    public function __wakeup()    {        echo "<br/>"."当反序列化时调用我 !".'<br/>';    }    public function __sleep(){        echo "<br/>"."当序列时调用我 !".'<br/>';        return array("name","sex","age");
    }    public function __call($arg1,$arg2){        echo  "<br/>"."当对象调用一个不存在或者不可访问的方法时调用我!".'<br/>';    }
}$D=new demo();   //实例化对象$D->num1(1,2);   //调用一个不存在的方法?>
   复制代码
 
结果输出:
 类被实例化时调用我!当对象调用一个不存在或者不可访问的方法时调用我!当类中的所有方法都被销毁时调用我!
   复制代码
 __set()
给不可访问属性赋值时,__set会被调用。
代码示例:
 <?phphighlight_file(__FILE__);class demo extends demo1{    public $name="xiaocui";    protected $sex="man";    private $age=26;    public function __construct()    {        echo "<br/>"."类被实例化时调用我!"."<br/>";    }    public function __destruct(){        echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";    }    public function person($per){        echo "<br/>"."We are $per !!!".'<br/>';    }    public function __set($arg1,$arg2){
        echo "<br/>"."当给不存在或者不可访问的属性赋值时调用我!"."<br/>";
    }}
class demo1{    private $weight;    public  $height;    public function people(){        echo $this->weight;        echo $this->height;    }}
$D=new demo();   //实例化对象$D->weight=74; //给不可访问的属性赋值?>
   复制代码
 
输出结果:
 类被实例化时调用我!当给不存在或者不可访问的属性赋值时调用我!当类中的所有方法都被销毁时调用我!
   复制代码
 __isset()
对不可访问属性调用isset()或empty()时,__iset()会被调用。
__unset()
对不可访问属性调用unset()时,__unset()会被调用。
__get()
读取不可访问属性的值时,__get会被调用。
代码示例:
 <?phphighlight_file(__FILE__);class demo extends demo1{    public $name="xiaocui";    protected $sex="man";    private $age=26;    public function __construct()    {        echo "<br/>"."类被实例化时调用我!"."<br/>";    }
    public function __destruct(){        echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";    }
    public function __get($arg1){        echo "<br/>"."读取不存在或不可访问的属性时调用我!";    }}
class demo1{    private $weight = 0;    public  $height;    public function people(){        echo $this->weight;        echo $this->height;    }}
$D=new demo();   //实例化对象
$D->weight;   //读取父类中不可访问的属性?>
   复制代码
 
输出结果:
 类被实例化时调用我!读取不存在或不可访问的属性时调用我!当类中的所有方法都被销毁时调用我!
   复制代码
 3.反序列化漏洞
3.1 反序列化漏洞利用条件
 ① unserialize()函数中参数可控② 存在可利用的类,且类中有魔术方法
   复制代码
 
代码示例 1:
 <?phphighlight_file(__FILE__);
class demo{
    public $arg1 = "0";
   public function __destruct()    {
        echo $this->arg1;  //输出用户传递的arg1值
    }
}
$a=$_GET['arg'];     //接收前端传递的arg1变量$unser = unserialize($a);    //反序列化传递的arg1?>
   复制代码
 
上述的代码满足反序列化漏洞的两个条件,arg参数可控,也就是unserialize()函数的参数可控;存在可利用的类demo,且 demo 类中有可利用的魔术方法__destruct(),然而__destruct()魔术方法中使用 echo 输出变量了arg。
通过arg参数,构造序列化字符串,通过unserialize()函数进行反序列化,最后通过__destruct()魔术方法输出变量,造成XSS。
示例代码 2
 <?phphighlight_file(__FILE__);
class demo{
    public $arg1 = "0";
    public function __destruct()    {
        eval($this->arg1);  //eval()去执行用户传递的arg1值
    }
}
$a=$_GET['arg'];     //接收前端传递的arg1变量$unser = unserialize($a);    //反序列化传递的arg1var_dump($unser);?>
   复制代码
 
如果魔术方法中存在eval(),且参数可控,那么通过构造序列化字符串,通过反序列化漏洞造成RCE。
3.2 __wakeup()函数绕过
在序列化时传递对象的属性大于实际对象的属性时,__wakeup()魔术方法将不会被执行,从而导致绕过。
PHP 版本<=5.6.25 或者 PHP 版本<=7.0.11
示例代码:
 <?phphighlight_file(__FILE__);
class demo{
    public $arg1 = "0";
    public function __destruct()    {
       eval($this->arg1);  //eval()去执行用户传递的arg1值
    }
    public function __wakeup(){        foreach(get_object_vars($this) as $k => $v) {                $this->$k = '';          //将传入的参数遍历,全部赋值为空            }
    }
}
$a=$_GET['arg'];     //接收前端传递的arg1变量$unser = unserialize($a);    //反序列化传递的arg1var_dump($unser);?>
   复制代码
 
如果属性值与对象实际属性值相同,则会在反序列化时执行__wakeup()函数将传入的变量值替换为空。
如果属性值设置大于实际对象属性值则会绕过__wakeup()函数。
评论