PHP反序列化漏洞 什么是序列化 魔术方法及绕过 修改类型 逃逸覆盖

为了储存对象,serialize产生一个可存储的值的表示(字符串),并且可以通过unserialize对字符串进行解析。

由于PHP本身对数据类型不敏感,不同类型对数据可以对变量进行赋值,反序列化过程由序列化字符串提供对象数量、对象数据类型、对象长度等信息,因此存在利用。

<?php

    class foo{
        public $a = 1;
        protected $b = 1;
        private $c = 3;
    }
    $tmp = new foo();
    echo serialize($tmp);
// O:3:"foo":3:{s:1:"a";i:1;s:4:"�*�b";i:1;s:6:"�foo�c";i:3;}
访问类型 变量名 序列化后变量长度 备注
public a 1
protected b 1 + 3
private c 1 + 5

不同访问类型的序列化结果是不同的,但PHP7.1版本对数据访问类型不敏感,可使用public代替其他类型数据

魔术方法及绕过

方法 触发 用途 绕过方式 备注
__sleep() serialize() 过滤不必要的对象存储,只存储数组中的变量名
__wakeup() unserialize() 用于反序列化调用之前初始化参数 修改序列化字符串中类含有的对象个数,当个数与类不匹配则不触发
__tostring() echo
<?php

    class foo{
        public $a;
        protected $b;
        private $c;
        public function __construct()
        {
            $this->a = 1;
            $this->b = 1;
            $this->c = 3;
            echo "__construct
";
        }
        public function __destruct()
        {
            var_dump($this);
            echo "__destruct
";
        }

        public function __wakeup()
        {
            echo "__wakeup()
";
        }
        public function __sleep()
        {
            echo "__sleep()
";
            return array('a','b');
        }
        public function __toString()
        {
            echo "__toString()
";
            return "";
        }
    }
    $tmp = new foo();
    echo $tmp;
    echo "serialize start:
";
    $tmp = serialize($tmp);
    echo "unserialize start:
";
    unserialize($tmp);

/*
__construct
__toString()
serialize start:
__sleep()
object(foo)#1 (3) {
  ["a"]=>
  int(1)
  ["b":protected]=>
  int(1)
  ["c":"foo":private]=>
  int(3)
}
__destruct
unserialize start:
__wakeup()
object(foo)#1 (3) {
  ["a"]=>
  int(1)
  ["b":protected]=>
  int(1)
  ["c":"foo":private]=>
  NULL
}
__destruct
*/

注意反序列化是不会调用构造函数的

修改类型

<?php
    class test1{

    }

    class test2{
          public function __wakeup(){
           // echo "flag";
          }
    }
    class foo{
        public $a;
        protected $b;
        private $c;
        public function __construct()
        {
            $this->a = 1;
            $this->b = 1;
            $this->c = new test1();
        }
    }
    $tmp = new foo();
    var_dump($tmp);
    echo "serialize start:
";
    $tmp = serialize($tmp);
    echo $tmp . "
";
    echo "unserialize start:
";
    $tmp = unserialize('O:3:"foo":3:{s:1:"a";i:1;s:4:"�*�b";i:1;s:6:"�foo�c";O:5:"test2":0:{}}');
    var_dump($tmp);

/*
object(foo)#1 (3) {
  ["a"]=>
  int(1)
  ["b":protected]=>
  int(1)
  ["c":"foo":private]=>
  object(test1)#2 (0) {
  }
}
serialize start:
O:3:"foo":3:{s:1:"a";i:1;s:4:"�*�b";i:1;s:6:"�foo�c";O:5:"test1":0:{}}
unserialize start:
object(foo)#1 (3) {
  ["a"]=>
  int(1)
  ["b":protected]=>
  int(1)
  ["c":"foo":private]=>
  object(test2)#2 (0) {
  }
}
*/

实验中,变量类test1,可以替换为类test2,并与类test2中的魔术方法一起配合达到攻击的目的

逃逸覆盖

反序列化过程字符串的截取长度是由序列中给出的长度决定的,利用这一点可以伪造

<?php
    class foo{
        public $a = "aaaaa";
        public $b = "bbbbb";
        public $c = "ccccc";
    }
    $tmp = new foo();
    $tmp = serialize($tmp);
    echo  $tmp;
    // O:3:"foo":3:{s:1:"a";s:5:"aaaaa";s:1:"b";s:5:"bbbbb";s:1:"c";s:5:"ccccc";}

    $payload  = 'bbbbb";s:1:"c";s:5:"ddddd";}';
    $ser = 'O:3:"foo":3:{s:1:"a";s:5:"aaaaa";s:1:"b";s:5:"'.$payload.'";s:1:"c";s:5:"ccccc";}';
    var_dump(unserialize($ser));
/*
{
  ["a"]=>
  string(5) "aaaaa"
  ["b"]=>
  string(5) "bbbbb"
  ["c"]=>
  string(5) "ddddd"
}
*/
  1. 使字符串长度s变小
  2. 使字符串本身经过replace后变长