BUUOJ新年红包题

预计阅读时间: 2 分钟

这道题是赵总新年发在BUUOJ的题(赵总NB!)

最后的Payload可以参考赵总的文章(https://www.zhaoj.in/read-6397.html)
本文只记录思路

题目改编自高校运维赛的ezpop,ezpop改编自TP6.0的反序列化利用gadget

题目如下:

<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }

    public function cleanContents(array $contents) {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete]);
    }

    public function save() {
        $contents = $this->getForStorage();

        $this->store->set($this->key, $contents, $this->expire);
    }

    public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        // 使缓存文件名随机
        $cache_filename = $this->options['prefix'] . uniqid() . $name;
        if(substr($cache_filename, -strlen('.php')) === '.php') {
            die('?');
        }
        return $cache_filename;
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (Exception $e) {
                // 创建失败
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //数据压缩
            $data = gzcompress($data, 3);
        }

        $data = "<?phpn//" . sprintf('%012d', $expire) . "n exit();?>n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return $filename;
        }

        return null;
    }

}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

一、如何RCE

关于RCE,这里实际上有两条思路。

1、利用file_put_contents()

在B类的set方法中,我们可以发现这里有一个写文件的操作,根据P牛的文章(https://www.leavesongs.com/PENETRATION/php-filter-magic.html),在文件名可控的情况下,通过加exit的方式存储php是不安全的

PS:这里有一点我之前一直有一个知识盲区。PHP中伪协议的php://流的解析是在函数(语法结构)中完成的,例如include,file_get_contents(),file_put_contents(),以及phar://伪协议实现反序列化等等。。

根据P神的文章,我们需要检查文件名是否可控,也就是是否可以使用伪协议。
在file_put_contents()中,$filename参数来自$filename =$this->getCacheKey($name);,这个函数定义如下

public function getCacheKey(string $name): string {
        // 使缓存文件名随机
        $cache_filename = $this->options['prefix'] . uniqid() . $name;
        if(substr($cache_filename, -strlen('.php')) === '.php') {
            die('?');
        }
        return $cache_filename;
    }

首先可以看到,通过改变
$this->options['prefix']的值,可以控制文件名的前半部分,也就是可以引入伪协议,利用条件成立。
然而这里有两个限制条件,uniqid使文件名随机化,而且做了限制使文件后缀不能为.php。
首先,uniqid也可以通过控制后半部分$name参数的值,加入/../来绕过;php的后缀限制绕过同样存在两种方法:第一种是通过在最后加入/.来绕过,第二种方式是通过上传.htaccess.user.ini来进行绕过。

2、利用重写的serialize方法

注意到在set方法中,$data = $this->serialize($value);这一段中所调用的serialize是本类中自己写的方法。
跟进这个方法

protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

发现这里存在动态调用,$serialize完全可控,考虑能不能找到一个函数进行RCE。
检查$data函数参数,发现$value来自A类中getForStorage()方法,该方法返回值是经过json_encode的string,无法直接作为RCE函数的参数。
这里要用到shell的一个小技巧:当shell执行的命令中存在'``'(反引号)时,反引号中的命令会被优先执行。
利用这个特性,可以将参数构造为

[`whoami`]


就可以达到RCE并且带出数据的效果

References:

本题出题人赵总的博客:https://www.zhaoj.in/read-6397.html
参考WP之一:http://althims.com/2020/01/29/buu-new-year/
原题目WP:https://250.ac.cn/2019/11/21/2019-EIS-WriteUp/#ezpop
Thinkphp 6.0 Gadget构造:https://www.anquanke.com/post/id/194036

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注