AIS3 EOF Quals 2026 write-up
Welcome

在 announcement 找到 flag

flag: EOF{2026-quals-in-2025}
misc
fun

這題是一個 eBPF 的 reverse 題

flag.enc: eabbc25677f3084458f0531f86863f226c95d555e6c28bd7 (32 bytes)
使用 llvm-objdump 查看

llvm-objdump -d xdp_prog.o:
1 | 0000000000000000 <xdp_encoder>: |
我們會發現他是對每一個字元用不同的 secret key 去做加密,所以我們只要找到 asm 內有做 XOR 的 key 並蒐集到 32 個bytes 即可
exp:
1 | enc_hex = "eabbc25677f3084458f0531f86863f226c95d555e6c28bd7" |
flag: EOF{si1Ks0Ng_15_g0oD_T0}
SaaS

seccomp-sandbox.c:
1 |
|
這裡會發現存在 TOCTOU 漏洞
這裡會導致 Kernel 必須重新回到使用者記憶體去讀取檔案路徑參數

所以我們可以透過 Sandbox 檢查完記憶體認為安全後,Kernel 真正執行時需同一塊記憶體,利用這微小的時間差將路徑偷換成 /flag

exp.c:
1 | #define _GNU_SOURCE |
flag: EOF{TICTACTOE_TICKTOCTOU}
Reverse
Structured - Small
題目給了 10 個 binary 一個一個分析會發現每一個 binary 幾乎除了 v5 的值 其他code 均相同, 但是會發現第 6 10 11 的 binary 是 flag 的開頭與結尾,裡面有做一些位移,所以我們要把 flag 的位移依照程式邏輯轉回來
exp.py:
1 | import struct |
bored
a part of signal.vcd:
1 | #833328 -> 訊號變 0 |
所以可以做出以下表格統計
| Bit 順序 | 時間點 (ns) | 讀取值 | 說明 |
|---|---|---|---|
| Start | 833,328 | 0 | 起始位元,通訊開始 |
| Bit 0 | 833,328 + 104,166 = 937,494 | 0 | 資料第 1 位 |
| Bit 1 | 937,494 + 104,166 = 1,041,660 | 1 | 資料第 2 位(波形拉高) |
| Bit 2 | 1,041,660 + 104,166 = 1,145,826 | 0 | 資料第 3 位(波形拉低) |
| Bit 3 | 1,145,826 + 104,166 = 1,249,992 | 0 | 資料第 4 位 |
| Bit 4 | 1,249,992 + 104,166 = 1,354,158 | 0 | 資料第 5 位 |
| Bit 5 | 1,354,158 + 104,166 = 1,458,324 | 1 | 資料第 6 位(波形拉高) |
| Bit 6 | 1,458,324 + 104,166 = 1,562,490 | 1 | 資料第 7 位 |
| Bit 7 | 1,562,490 + 104,166 = 1,666,656 | 0 | 資料第 8 位(波形拉低) |
| Stop | 1,666,656 + 104,166 = 1,770,822 | 1 | 結束位元 |
現在我們收集到了 8 個資料位元: 0, 1, 0, 0, 0, 1, 1, 0
因為 UART 協定是 LSB First(最低有效位先傳),所以要把這個順序倒過來讀,才能變成人類看得懂的二進位數字:
- 原始順序: 0 1 0 0 0 1 1 0
- 倒轉順序 (MSB): 0 1 1 0 0 0 1
將二進位 01100010 轉換成十六進位:
- 0110 = 6
- 0010 = 2
結果 = 0x62
0x62 = b
接著只要用同樣的邏輯(尋找下一個 Start Bit 0,然後每隔 104,166ns 讀一次)
依此類推,最終就會拼湊出key: b4r3MEt41

flag: EOF{ExP3d14i0N_33_15_4he_G0AT}
Web
Bun.PHP
這題是一個使用 bun v1.3.5 運行的 Server,不是傳統 nginx / Apache
Dockerfile: FROM oven/bun:1.3.5-alpine
我們可以從 index.js 發現
伺服器有一個路由 /cgi-bin/:filename,他限制我們請求的路徑只能以 .php 結尾,並且他是以 bun shell 的方式 $ 執行

雖然是 php 寫的,但是透過 Shebang #!/usr/bin/php-cgi 執行的
解決限制路徑的方式就是透過 path traversal 的方式 …%2f…%2f 來繞過
並且強制以 .php 結尾,我們可以透過 Null Byte injection (%00) 去截斷,這樣也確實是以 .php 結尾,但是又可以截斷 .php
這樣我們就可以嘗試執行 /bin/sh -> ..%2f..%2f..%2f..%2f..%2fbin%2fsh%00.php
於是我們可以使用 curl 送進我們的 payload,達成 RCE
final payload: curl -X POST "https://139b064eaa7c424a.chal.eof.133773.xyz:20001/cgi-bin/..%2f..%2f..%2f..%2fbin%2fsh%00.php" --data-binary $'printf "\r\n\r\n"; /readflag give me the flag'
flag: EOF{1_tUrn3d_Bun.PHP_Int0_4_r34l1ty}
LinkoReco
docker-compose.yml:
1 | version: '3' |
Hints:
1.
1 | HINT: FULL ACCESS TOKEN CAN ONLY BE OBTAINED THROUGH LOCALHOST(web) |
1 | 1. Token 只有在使用者從 local 造訪的時候會顯示 |
1 | Cache Rule |
challenge page:

這一題我們要利用快取機制,來取得只有 localhost 才能拿到的 Token
再來需要利用拿到的 Token 去做驗證,在利用 SSRF 讀取 Server 內部的檔案
取得 Token 方法:
- 根據 Nginx 的 map 規則
"~*^/static/.*\.(svg|png|jpg|jpeg|css)$" 1;,這表示如果網址以 /static/ 開頭且以圖片或 CSS 副檔名結尾,Nginx 會將其視為靜態資源並進行快取 - 但是 hint 裡有提到 /static 的檔案直接走 nginx,但因為「AI 寫壞了」,如果檔案不存在,通常 Nginx 的 try_files 機制會將請求轉給 index.php 處理
- 在 url 輸入欄填入:
http://web/static/..%2findex.php/<anything>.png
然後使用payload:
curl -H "Host: web" --path-as-is http://chals1.eof.ais3.org:19080/static/..%2findex.php/flag.png
我們就可以拿到 Token 了

為啥可以這樣?
根據 Nginx 的判斷,Nginx 檢查網址字串:/static/..%2findex.php/flag.png
Nginx 裡面寫:只要是 /static/ 開頭且以 .png 結尾,就視為靜態檔案
結果就是: 這是一個靜態圖片,應該被快取
但當 Nginx 真的要去抓檔案的話,路徑就變成了/static/../index.php/flag.png
/static/…/ 互相抵消,最終路徑指向 /index.php,後面的 /flag.png 變成 PATH_INFO,被 php 忽略了
執行結果:Nginx 將請求轉交給 PHP 執行 index.php
既然我們有了 Token ,我們可以去執行 Server 內的任意讀,利用 file://,但是我們會發現 docker-compose.yml,裡面把 flag.txt 命名成不知道是啥東東,所以我們可以利用 file:///proc/self/mounts 去找他裡面掛載了啥東東

發現 ca7_f113.txt
所以使用 file:///etc/ca7_f113.txt
get flag!

flag: EOF{たきな、スイーツ追加!それがないなら……修理?やらないから!}