PHP反序列化学习

入门之前

首先要知道PHP是一门面向对象的语言

所以会碰到类,对象之类的概念

稍微补充一点罢,不然会听不懂的()

  • − 定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作。
  • 对象 − 是类的实例。
  • 成员变量 − 定义在类内部的变量。该变量的值对外是不可见的,但是可以通过成员函数访问,在类被实例化为对象后,该变量即可成为对象的属性
  • 成员函数 − 定义在类的内部,可用于访问对象的数据。
  • 继承 − 继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
  • 父类 − 一个类被其他类继承,可将该类称为父类,或基类,或超类。
  • 子类 − 一个类继承其他类称为子类,也可称为派生类。
  • 多态 − 多态性是指相同的函数或方法可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。
  • 重载 − 简单说,就是函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
  • 抽象性 − 抽象性是指将具有一致的数据结构(属性)和行为(操作)的对象抽象成类。一个类就是这样一种抽象,它反映了与应用有关的重要性质,而忽略其他一些无关内容。任何类的划分都是主观的,但必须与具体的应用有关。
  • 封装 − 封装是指将现实世界中存在的某个客体的属性与行为绑定在一起,并放置在一个逻辑单元内。
  • 构造函数 − 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
  • 析构函数 − 析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做”清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,应在退出前在析构函数中用delete释放)。
<?php
class Site {//类
  /* 成员变量 */
  var $url;
  var $title;
  
  /* 成员函数 */
  function setUrl($par){
     $this->url = $par;
  }
  
  function getUrl(){
     echo $this->url . PHP_EOL;
  }
  
  function setTitle($par){
     $this->title = $par;
  }
  
  function getTitle(){
     echo $this->title . PHP_EOL;
  }
}
$a=new Site();//创建对象
$a->setTitle('AyaN0');//调用成员方法
//或者$a->setTitle='AyaN0';

?>

pop链构造

__construct()__destruct()

__construct:当对象创建时会自动调用,注意是创建的时候,也就是说有new的时候就会调用,在unserialize时是不会被自动调用的

__destruct()`:当对象被销毁时会自动调用;当新对象创建后,它后面一定会被自动销毁,也就是调用`__construct`后一定会调用`__destruct`;或者我们直接传入一个对象,它后面被销毁时也会调用`__destruct

可以看到,创建对象e时调用了__construct,然后输出序列化后的对象t,最后在销毁对象t时调用了__destruct

  • 实例

    <?php
    class User{
    
        public $username;
    
        public function __construct($username)
        {
            $this->username = $username;
            echo "__construct test";
        }
    
    }
    $test = new User("F0rmat");
    $ser = serialize($test);
    unserialize($ser);
    ?>
    //__construct test
    <?php
    class User{
    
        public function __destruct()
        {
            echo "__destruct test</br>";
        }
    
    }
    $test = new User();
    $ser = serialize($test);
    unserialize($ser);
    ?>
    //__destruct test
    //__destruct test

__sleep()__wakeup()

__sleep() :在对象被序列化之前被调用,就是说看到serialize时就会被调用,而且是先调用后再执行序列化

__wakeup(): 将在字符串被反序列化之后被立即调用,就是说看到unserialize后就会被立即调用

在看到serialize($b)后,它是先调用了__sleep()魔法函数,然后才执行了echo,输出了字符串

直接输入了字符串,当它执行了unserialize转换成对象后,就会最先调用__wakeup(),它的优先级最高

<?php
class User{
    const SITE = 'uusama';

    public $username;
    public $nickname;
    private $password;

    public function __construct($username, $nickname, $password)
    {
        $this->username = $username;
        $this->nickname = $nickname;
        $this->password = $password;
    }

    // 重载序列化调用的方法
    public function __sleep()
    {
        // 返回需要序列化的变量名,过滤掉password变量
        return array('username', 'nickname');
    }

}
$user = new User('a', 'b', 'c');
echo serialize($user);
//O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}结果就是过滤掉了password的值
<?php
class User{
    const SITE = 'uusama';

    public $username;
    public $nickname;
    private $password;
    private $order;

    public function __construct($username, $nickname, $password)
    {
        $this->username = $username;
        $this->nickname = $nickname;
        $this->password = $password;
    }

    // 定义反序列化后调用的方法
    public function __wakeup()
    {
        $this->password = $this->username;
    }
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));
/*
class User#1 (4) {
  public $username =>
  string(1) "a"
  public $nickname =>
  string(1) "b"
  private $password =>
  string(1) "a"
  private $order =>
  NULL
}
上面wakeup的函数作用是将username的变量值赋值给password变量。*/

__toString()

__toString()魔术方法是最为最要的,在构造pop链中它往往是很关键的一环,在很多种情况下都会被调用,主要是下面这些:

  1. echo($obj)print($obj)打印对象时会触发
  2. 反序列化对象与字符串连接时
  3. 反序列化对象参与格式化字符串时
  4. 反序列化对象字符串进行preg_match正则匹配),因为php进行弱比较时会转换参数类型,相当于都转换成字符串进行比较
  5. 反序列化对象参与格式化sql语句时,绑定参数时(用的少)
  6. 反序列化对象经过php字符串函数时,如strlen()addslashes()时(用的少)
  7. in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有tostring返回的字符串的时候tostring会被调用
  8. 反序列化对象作为class_exists()的参数的时候(用的少)

通过看它被调用的情况,不难总结出,当对象被当成了字符串的时候,__toString()就会被调用,无论是将对象打印出来,还是将对象去与字符串进行比较,它都会被调用;这里要注意的是,必须要操作的是对象的时候,才会被调用

新建了对象t就直接打印它,照理说肯定是不会有任何回显的,因为只有字符串能被打印,对象肯定是不能被直接打印的,需要先将它序列化成字符串后才可以打印;但我们这直接打印发现它居然有输出,就是因为它按照操作字符串的方法去操作了对象,所以说调用了__toString(),然后将它的返回值输出了出来

<?php
class User{

    public function __toString()
    {
       return '__toString test';
    }

}

$test = new User();
echo $test;
//__toString

__invoke()

__invoke:当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用,而调用函数的方式就是在后面加上(),当我们看到像return $function();这种语句时,就应该意识到后面可能会调用__invoke(),下图是直接在对象后面加()调用

需要注意的是,这个魔术方法只在PHP 5.3.0 及以上版本有效

<?php
class User{

    public function __invoke()
    {
       echo '__invoke test';
    }

}

$test = new User();
$test();
//__invoke test

__get()__set()

__get():从不可访问的属性中读取值,或者说是调用一个类及其父类方法中未定义属性时,需要一个参数,代表不存在的属性值

__set():当给一个未定义的属性赋值时,或者修改一个不能被修改的属性时(private protected)(用的不多)

echo语句调用了__toString(),然后它返回的是当前对象的t属性,但我们是没有定义t这个属性的,所以说会调用__get(),然后将返回值打印出来

<?php
class User{
    public $var1;
    public  function __get($arg1)
    {
        echo $arg1;
    }

}
$test = new User();
$test->var2;
?>
    //var2
<?php
class User{
    public $var1;
    public  function __set($arg1,$arg2)
    {
        echo $arg1.','.$arg2;
    }

}
$test = new User();
$test->var2=1;
?>

__call()__callStatic()

__call:在对象中调用类中不存在的方法时,或者是不可访问方法时被调用

__callStatic:在静态上下文中调用一个不可访问静态方法时被调用(用的不多)

比如说像这段代码,我们调用对象t中的方法t2,但因为类中没有方法t2,所以说就调用了__call()

<?php
class User{

    public function __call($arg1,$arg2)
    {
        echo "$arg1,$arg2[0]";
    }

}
$test = new User();
$test->callxxx('a');
?>
//callxxx,a
<?php
class User{

    public static function __callStatic($arg1,$arg2)
    {
        echo "$arg1,$arg2[0]";
    }

}
$test = new User();
$test::callxxx('a');
?>
//callxxx,a

这里先来学习一下双冒号的用法,双冒号也叫做范围解析操作符(也可称作 Paamayim Nekudotayim)或者更简单地说是一对冒号,可以用于访问静态成员,类常量,还可以用于覆盖类中的属性和方法。自 PHP 5.3.0 起,可以通过变量来引用类,该变量的值不能是关键字(如 self,parent 和 static)。与**__call不同的是需要添加static**,只有访问不存在的静态方法才会触发。

__clone()

__clone():当使用clone关键字拷贝完成一个对象后,新对象就会调用定义的魔术方法(如果存在)

<?php
class User{

    public function __clone()
    {
        echo "__clone test";
    }

}
$test = new User();
$newclass = clone($test);
?>
//__clone test

__isset()__unset

__isset():对不可访问属性调用isset()或者empty()时,__isset()会被调用

__unset():对不可访问属性调用unset()时,__unset()会被触发

<?php
class User{
    private $var;
    public  function __isset($arg1)
    {
        echo $arg1;
    }

}
$test = new User();
isset($test->var1);
?>
    //var1
<?php
class User{
    public  function __unset($arg1)
    {
        echo $arg1;
    }

}
$test = new User();
unset($test->var1);
?>
    //var1

绕过

绕过 __wakeup:

  • PHP5 < 5.6.25
  • PHP7 < 7.0.10

当序列化后对象的参数列表中成员个数和实际个数不符合时会绕过 __wakeup()

//详见[SWPUCTF 2021 新生赛]no_wakeup

当然,也可以通过赋值来绕过__wake下面将执行的字符串置空的情况

使另一个构造好的变量和被控制的变量公用一个内存如

$t->c="system('ls /');";
$t->b=&$t->a;
//a未被wakeup置空的变量,执行的也是a,前面还有一个c->b的过程

绕过 O

有时会对构造的payload进行正则匹配

此时需要绕过一些必须绕过的关键词,如O

gc机制

构链经验:

首先,找到注入点,注入点一般 被包裹在最内部(因为第一个执行,通过各种魔术方法将结果成功传输到外部并成功销毁对象),接着内层的魔术方法要在紧邻的外层得到触发,(也就是为什么他被称为pop链的缘故),最后成功销毁就算pop链构造成功了

phar反序列化