以下の内容はhttps://yocchin.hatenablog.com/entry/2025/06/10/075510より取得しました。


TJCTF 2025 Writeup

この大会は2025/6/6 21:00(JST)~2025/6/8 21:00(JST)に開催されました。
今回もチームで参戦。結果は8698点で723チーム中46位でした。
自分で解けた問題をWriteupとして書いておきます。

discord (misc)

Discordに入り、#generalチャネルのトピックを見ると、フラグが書いてあった。

tjctf{wahooooooooooooooooooooooo_sanity_check}

guess-my-number (misc)

1以上1000以下のランダムな値を当てる必要がある。10回チャンスがあり、入力値が当てるべき値より大きいか小さいかわかるので、二分探索で求める。

#!/usr/bin/env python3
import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('tjc.tf', 31700))

r = [1, 1000]
for i in range(10):
    guess = (r[0] + r[1]) // 2
    data = recvuntil(s, b': ')
    print(data + str(guess))
    s.sendall(str(guess).encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    if data == 'Too high':
        r[1] = guess
    elif data == 'Too low':
        r[0] = guess
    else:
        break

実行結果は以下の通り。

Guess a number from 1 to 1000: 500
Too low
Guess a number from 1 to 1000: 750
Too high
Guess a number from 1 to 1000: 625
Too high
Guess a number from 1 to 1000: 562
Too low
Guess a number from 1 to 1000: 593
Too low
Guess a number from 1 to 1000: 609
Too high
Guess a number from 1 to 1000: 601
Too high
Guess a number from 1 to 1000: 597
Too high
Guess a number from 1 to 1000: 595
You won, the flag is tjctf{g0od_j0b_u_gu33sed_correct_998}
tjctf{g0od_j0b_u_gu33sed_correct_998}

mouse-trail (misc)

添付のテキストファイルに記載されている各座標をプロットすると、その結果フラグが浮き上がった。

#!/usr/bin/env python3
import matplotlib.pyplot as plt

with open('mouse_movements.txt', 'r') as f:
    lines = f.read().splitlines()

x_vals = []
y_vals = []
for line in lines:
    x = int(line.split(',')[0])
    y = int(line.split(',')[1])
    x_vals.append(x)
    y_vals.append(y)

plt.scatter(x_vals, y_vals)
plt.gca().invert_yaxis()

plt.show()

tjctf{we_love_cartesian_plane}

guess-again (rev)

マクロを見てみる。

$ olevba chall.xlsm               
olevba 0.60.2 on Python 3.11.9 - http://decalage.info/python/oletools
===============================================================================
FILE: chall.xlsm
Type: OpenXML
WARNING  For now, VBA stomping cannot be detected for files in memory
-------------------------------------------------------------------------------
VBA MACRO ThisWorkbook.cls 
in file: xl/vbaProject.bin - OLE stream: 'VBA/ThisWorkbook'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
(empty macro)
-------------------------------------------------------------------------------
VBA MACRO Sheet1.cls 
in file: xl/vbaProject.bin - OLE stream: 'VBA/Sheet1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
(empty macro)
-------------------------------------------------------------------------------
VBA MACRO Module1.bas 
in file: xl/vbaProject.bin - OLE stream: 'VBA/Module1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

Sub CheckFlag()
    Dim guess As String
    guess = ActiveSheet.Shapes("TextBox 1").TextFrame2.TextRange.Text

    If Len(guess) < 7 Then
        MsgBox "Incorrect"
        Exit Sub
    End If

    If Left(guess, 6) <> "tjctf{" Or Right(guess, 1) <> "}" Then
        MsgBox "Flag must start with tjctf{ and end with }"
        Exit Sub
    End If

    Dim inner As String
    inner = Mid(guess, 7, Len(guess) - 7)
    
    Dim expectedCodes As Variant
    expectedCodes = Array(98, 117, 116, 95, 99, 52, 110, 95, 49, 116, 95, 114, 117, 110, 95, 100, 48, 48, 109)
    Dim i As Long
    If Len(inner) <> (UBound(expectedCodes) - LBound(expectedCodes) + 1) Then
        MsgBox "Incorrect"
        Exit Sub
    End If
    For i = 1 To Len(inner)
        If Asc(Mid(inner, i, 1)) <> expectedCodes(i - 1) Then
            MsgBox "Incorrect"
            Exit Sub
        End If
    Next i
    
    MsgBox "Flag correct!"
End Sub
    


Function check(str, arr, idx1, idx2) As Boolean
    If Mid(str, idx1, 1) = Chr(arr(idx2)) Then
        check = True
    Else
        check = False
End Function
-------------------------------------------------------------------------------
VBA MACRO Module2.bas 
in file: xl/vbaProject.bin - OLE stream: 'VBA/Module2'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
Function Validate(userInput As String) As Boolean
    Dim f As String
    f = "tjctf{fake_flag}"
    
    Validate = False

End Function


+----------+--------------------+---------------------------------------------+
|Type      |Keyword             |Description                                  |
+----------+--------------------+---------------------------------------------+
|Suspicious|Chr                 |May attempt to obfuscate specific strings    |
|          |                    |(use option --deobf to deobfuscate)          |
|Suspicious|Hex Strings         |Hex-encoded strings were detected, may be    |
|          |                    |used to obfuscate strings (option --decode to|
|          |                    |see all)                                     |
+----------+--------------------+---------------------------------------------+

このことからフラグは以下の条件をみたすことがわかる。

・"tjctf{"から始まり、"}"で終わる。
・フラグの各文字のACIIコードは、98, 117, 116, 95, 99, 52, 110, 95, 49, 116, 95, 114, 117, 110, 95, 100, 48, 48, 109となる。

このASCIIコードをデコードし、フラグの形式にすればフラグになる。

>>> s = [98, 117, 116, 95, 99, 52, 110, 95, 49, 116, 95, 114, 117, 110, 95, 100, 48, 48, 109]
>>> flag = ''.join([chr(c) for c in s])
>>> flag = 'tjctf{%s}' % flag
>>> flag
'tjctf{but_c4n_1t_run_d00m}'
tjctf{but_c4n_1t_run_d00m}

serpent (rev)

pickleとしてロードし、ソースコードデコンパイルする。

#!/usr/bin/env python3
import pickle
import ast

with open('ast_dump.pickle', 'rb') as f:
    data = pickle.load(f)

source_code = ast.unparse(data)
print(source_code)

復元したソースコードは次の通り。

import random
import base64
import hashlib
from typing import List, Dict, Optional, Union, Any

class DataProcessor:

    def __init__(self):
        self.data = []
        self.cache = {}
        self.config = {'debug': False, 'verbose': True, 'timeout': 30, 'retries': 3}

    def process_data(self, input_data: Any) -> Optional[str]:
        if not input_data:
            return None
        transformed = self._transform(input_data)
        validated = self._validate(transformed)
        return self._encode(validated) if validated else None

    def _transform(self, data: Any) -> str:
        fake_secret = 'not_the_flag'
        decoy_1 = 'tjctf{h4ck3r_t1m3}'
        decoy_2 = 'tjctf{c0d3_br34k3r}'
        decoy_3 = 'tjctf{s3cur1ty_f41l}'
        decoy_4 = 'tjctf{3xpl01t_m0d3}'
        decoy_5 = 'tjctf{p4ssw0rd_cr4ck}'
        decoy_6 = 'tjctf{syst3m_h4ck}'
        decoy_7 = 'tjctf{d4t4_br34ch}'
        decoy_8 = 'tjctf{n3tw0rk_p3n3tr4t3}'
        decoy_9 = 'tjctf{cyb3r_w4rr10r}'
        decoy_10 = 'tjctf{c0mput3r_v1rus}'
        decoy_11 = 'tjctf{m4lw4r3_d3t3ct}'
        decoy_12 = 'tjctf{f1r3w4ll_byp4ss}'
        decoy_13 = 'tjctf{r00tk1t_1nst4ll}'
        decoy_14 = 'tjctf{k3yl0gg3r_r3c0rd}'
        decoy_15 = 'tjctf{sp4m_b0t_4ct1v3}'
        decoy_16 = 'tjctf{ph1sh1ng_4tt4ck}'
        decoy_17 = 'tjctf{r4ns0mw4r3_3ncrypt}'
        decoy_18 = 'tjctf{tr0j4n_h0rs3}'
        decoy_19 = 'tjctf{w0rm_spr34d}'
        decoy_20 = 'tjctf{sp00f1ng_1d3nt1ty}'
        dummy_var = 'placeholder'
        return str(data).upper()

    def _validate(self, data: str) -> bool:
        patterns = ['^[A-Z0-9_]+$', '\\w+', '.*']
        return len(data) > 0

    def _encode(self, data: str) -> str:
        encoded = base64.b64encode(data.encode()).decode()
        return encoded

class OuterLayer:

    def __init__(self):
        self.data = [1, 2, 3, 4, 5] * 100
        self.cache = {str(i): i ** 2 for i in range(50)}
        self.metadata = {'version': '1.0', 'author': 'unknown'}

    def process_data(self, input_data):

        def level_one_nested():
            decoy_a = 'tjctf{m1n3cr4ft_w0rld}'
            decoy_b = 'tjctf{r0bl0x_g4m3}'
            decoy_c = 'tjctf{f0rtn1t3_v1ct0ry}'
            decoy_d = 'tjctf{l34gu3_0f_l3g3nds}'
            decoy_e = 'tjctf{c0d_w4rf4r3}'
            decoy_f = 'tjctf{4m0ng_us_sus}'
            decoy_g = 'tjctf{f4ll_guy5_w1n}'
            decoy_h = 'tjctf{r0ck3t_l34gu3}'
            decoy_i = 'tjctf{0v3rw4tch_h3r0}'
            decoy_j = 'tjctf{v4l0r4nt_4g3nt}'

            class MiddleLayer:

                def __init__(self):
                    self.values = list(range(1000))
                    self.lookup = {chr(65 + i): i for i in range(26)}

                def deep_process(self):
                    dummy_1 = 'tjctf{sp4c3_1nv4d3rs}'
                    dummy_2 = 'tjctf{p4c_m4n_g4m3}'
                    dummy_3 = 'tjctf{t3tr1s_bl0cks}'
                    dummy_4 = 'tjctf{sup3r_m4r10}'
                    dummy_5 = 'tjctf{d0nk3y_k0ng}'
                    dummy_6 = 'tjctf{str33t_f1ght3r}'
                    dummy_7 = 'tjctf{m0rt4l_k0mb4t}'
                    dummy_8 = 'tjctf{z3ld4_l1nk}'
                    dummy_9 = 'tjctf{p0k3m0n_c4tch}'
                    dummy_10 = 'tjctf{f1n4l_f4nt4sy}'

                    def level_two_nested():
                        red_herring_1 = 'tjctf{sk1b1d1}'

                        class InnerLayer:

                            def __init__(self):
                                self.config = {'debug': False, 'verbose': True}
                                self.settings = {'timeout': 30, 'retries': 5}

                            def ultra_deep_process(self):
                                fake_inner_1 = 'tjctf{days_b4_111}'

                                def level_three_nested():
                                    distraction_1 = 'tjctf{60mgs}'

                                    class CoreLayer:

                                        def __init__(self):
                                            self.core_data = [x ** 3 for x in range(100)]
                                            self.core_cache = {}

                                        def final_process(self):
                                            noise_1 = 'tjctf{4everbody}'

                                            def level_four_nested():
                                                garbage_1 = 'tjctf{b4l3nc14ga_pr1nc3ss}'

                                                class DeepestLayer:

                                                    def __init__(self):
                                                        self.final_data = {'key': 'value'}

                                                    def ultimate_process(self):
                                                        junk_1 = 'tjctf{junk_1}'
                                                        junk_2 = 'tjctf{junk_2}'
                                                        junk_3 = 'tjctf{junk_3}'

                                                        def final_nested():
                                                            secret = 'tjctf{f0ggy_d4ys}'
                                                            return None
                                                        return final_nested()
                                                return DeepestLayer()
                                            return level_four_nested()

                                        def another_method(self):
                                            more_noise_1 = 'tjctf{more_noise_1}'
                                            more_noise_2 = 'tjctf{more_noise_2}'
                                            return 'nothing'
                                    return CoreLayer()
                                return level_three_nested()

                            def secondary_method(self):
                                secondary_fake_1 = 'tjctf{secondary_fake_1}'
                                secondary_fake_2 = 'tjctf{secondary_fake_2}'
                                return 'secondary'
                        return InnerLayer()
                    return level_two_nested()

                def alternate_method(self):
                    alternate_fake_1 = 'tjctf{alternate_fake_1}'
                    alternate_fake_2 = 'tjctf{alternate_fake_2}'
                    return 'alternate'
            return MiddleLayer()
        return level_one_nested()

def calculate_fibonacci(n: int) -> List[int]:
    if n <= 0:
        return []
    elif n == 1:
        return [0]
    elif n == 2:
        return [0, 1]
    fib = [0, 1]
    for i in range(2, n):
        fib.append(fib[i - 1] + fib[i - 2])
    return fib

def hash_generator(text: str) -> Dict[str, str]:
    hashes = {}
    hashes['md5'] = hashlib.md5(text.encode()).hexdigest()
    hashes['sha1'] = hashlib.sha1(text.encode()).hexdigest()
    hashes['sha256'] = hashlib.sha256(text.encode()).hexdigest()
    fake_flag_1 = 'tjctf{5h4d0w_run3r}'
    fake_flag_2 = 'tjctf{d4rk_m4g1c}'
    fake_flag_3 = 'tjctf{n1ght_cr4wl3r}'
    fake_flag_4 = 'tjctf{3v1l_sp1r1t}'
    fake_flag_5 = 'tjctf{bl4ck_h0l3}'
    fake_flag_6 = 'tjctf{v01d_w4lk3r}'
    fake_flag_7 = 'tjctf{d34th_st4r}'
    fake_flag_8 = 'tjctf{ch40s_l0rd}'
    fake_flag_9 = 'tjctf{w1ck3d_m1nd}'
    fake_flag_10 = 'tjctf{d00m_br1ng3r}'
    fake_flag_11 = 'tjctf{3v1l_g3n1us}'
    fake_flag_12 = 'tjctf{h3ll_sp4wn}'
    fake_flag_13 = 'tjctf{d4rk_k1ng}'
    fake_flag_14 = 'tjctf{s4t4n1c_c0d3}'
    fake_flag_15 = 'tjctf{d3m0n_h4ck3r}'
    fake_flag_16 = 'tjctf{v4mp1r3_byt3s}'
    fake_flag_17 = 'tjctf{gh0st_1n_sh3ll}'
    fake_flag_18 = 'tjctf{z0mb13_c0d3}'
    fake_flag_19 = 'tjctf{sk3l3t0n_k3y}'
    fake_flag_20 = 'tjctf{wr41th_m0d3}'
    return hashes

def obfuscated_function():
    x = 42
    y = 'hello'
    z = [1, 2, 3, 4, 5]
    ocean_flags = ['tjctf{d33p_s34s}', 'tjctf{blu3_w4v3s}', 'tjctf{0c34n_curr3nt}', 'tjctf{s4lty_w4t3r}', 'tjctf{m4r1n3_l1f3}', 'tjctf{c0r4l_r33f}', 'tjctf{wh4l3_s0ng}', 'tjctf{t1d4l_p00l}', 'tjctf{sh4rk_4tt4ck}', 'tjctf{s34_m0nst3r}']
    space_flags = ['tjctf{st4r_d4nc3r}', 'tjctf{g4l4xy_r1d3r}', 'tjctf{c0sm1c_w1nd}', 'tjctf{pl4n3t_h0p}', 'tjctf{n3bul4_dr1ft}', 'tjctf{4st3r01d_b3lt}', 'tjctf{bl4ck_h0l3}', 'tjctf{sup3rn0v4}', 'tjctf{m3t30r_sh0w3r}', 'tjctf{sp4c3_d3br1s}']
    for i in range(10):
        temp = i * 2
        if temp % 3 == 0:
            temp += 1
    nested_dict = {'level1': {'level2': {'level3': {'data': 'nothing important', 'flag': 'tjctf{d33p_f4k3}', 'ocean': ocean_flags, 'space': space_flags}}}}
    return nested_dict

def complex_logic():
    items = ['apple', 'banana', 'cherry', 'date']
    processed = []
    for item in items:
        if len(item) > 4:
            processed.append(item.upper())
        else:
            processed.append(item.lower())
    return processed

class ConfigManager:

    def __init__(self):
        self.settings = {'api_key': 'fake_key_12345', 'endpoint': 'https://api.example.com', 'version': '1.0.0'}
        self.gaming_flags = ['tjctf{g4m3r_m0d3}', 'tjctf{l3v3l_up}', 'tjctf{p0w3r_pl4y3r}', 'tjctf{b0ss_f1ght}', 'tjctf{qu3st_c0mpl3t3}', 'tjctf{l00t_dr0p}', 'tjctf{cr1t1c4l_h1t}', 'tjctf{sp33d_run}', 'tjctf{n0_scr1pt_k1dd13}', 'tjctf{pr0_pl4y3r}']
        self.music_flags = ['tjctf{b34t_dr0p}', 'tjctf{s0und_w4v3}', 'tjctf{m3l0dy_m4k3r}', 'tjctf{rh7thm_rush}', 'tjctf{4ud10_f1l3}', 'tjctf{d4nc3_fl00r}', 'tjctf{v0lum3_up}', 'tjctf{b4ss_b00st}', 'tjctf{t3mp0_ch4ng3}', 'tjctf{s0ng_qu3u3}']

    def get_setting(self, key: str) -> Optional[str]:
        return self.settings.get(key)

    def update_setting(self, key: str, value: str) -> None:
        self.settings[key] = value

def random_data_generator(size: int) -> List[int]:
    return [random.randint(1, 100) for _ in range(size)]

def string_manipulator(text: str) -> str:
    operations = [lambda x: x.upper(), lambda x: x.lower(), lambda x: x[::-1], lambda x: x.replace('a', '@'), lambda x: x.replace('e', '3')]
    result = text
    for op in operations:
        result = op(result)
    return result

def nested_loops_example():
    matrix = []
    for i in range(5):
        row = []
        for j in range(5):
            value = i * j
            if value % 2 == 0:
                row.append(value)
            else:
                row.append(value + 1)
        matrix.append(row)
    return matrix

def exception_handler():
    try:
        risky_operation = 10 / 0
    except ZeroDivisionError:
        fallback_value = 'error handled'
        return fallback_value
    except Exception as e:
        generic_error = str(e)
        return generic_error
    finally:
        cleanup_code = 'always executed'

def recursive_function(n: int) -> int:
    if n <= 1:
        return 1
    return n * recursive_function(n - 1)

def massive_red_herring_factory():
    sport_flags = ['tjctf{f00tb4ll_h3r0}', 'tjctf{b4sk3tb4ll_l3g3nd}', 'tjctf{s0cc3r_st4r}', 'tjctf{b4s3b4ll_ch4mp}', 'tjctf{t3nn1s_4c3}', 'tjctf{g0lf_m4st3r}', 'tjctf{sw1mm1ng_f4st}', 'tjctf{runn1ng_sp33d}', 'tjctf{cycl1ng_r4c3}', 'tjctf{sk41ng_tr1ck}']
    vehicle_flags = ['tjctf{c4r_3ng1n3}', 'tjctf{m0t0rcycl3_r1d3}', 'tjctf{tr4in_st4t10n}', 'tjctf{4irpl4n3_fl1ght}', 'tjctf{b04t_s41l}', 'tjctf{subm4r1n3_d1v3}', 'tjctf{h3l1c0pt3r_s0und}', 'tjctf{r0ck3t_l4unch}', 'tjctf{sp4c3sh1p_0rb1t}', 'tjctf{b1cycl3_p3d4l}']
    instrument_flags = ['tjctf{gu1t4r_s0l0}', 'tjctf{p14n0_k3ys}', 'tjctf{drum_b34t}', 'tjctf{v10l1n_str1ng}', 'tjctf{fl ut3_m3l0dy}', 'tjctf{s4x0ph0n3_j4zz}', 'tjctf{tr0mb0n3_sl1d3}', 'tjctf{cl4r1n3t_w00d}', 'tjctf{h4rp_4ng3l}', 'tjctf{0rg4n_p1p3}']
    return sport_flags + vehicle_flags + instrument_flags

def another_distraction_layer():
    job_flags = ['tjctf{d0ct0r_h34l}', 'tjctf{t34ch3r_l34rn}', 'tjctf{3ng1n33r_bu1ld}', 'tjctf{l4wy3r_d3f3nd}', 'tjctf{ch3f_c00k}', 'tjctf{p1l0t_fly}', 'tjctf{n urs3_c4r3}', 'tjctf{f1r3f1ght3r_s4v3}', 'tjctf{p0l1c3_pr0t3ct}', 'tjctf{4rt1st_cr34t3}']
    emotion_flags = ['tjctf{h4ppy_f33l}', 'tjctf{s4d_t34r}', 'tjctf{4ngry_r4g3}', 'tjctf{3xc1t3d_j0y}', 'tjctf{n3rv0us_w0rry}', 'tjctf{pr0ud_w1n}', 'tjctf{j34l0us_3nvy}', 'tjctf{c0nf1d3nt_str0ng}', 'tjctf{l0n3ly_4l0n3}', 'tjctf{c4lm_p34c3}']
    country_flags = ['tjctf{4m3r1c4_us4}', 'tjctf{c4n4d4_m4pl3}', 'tjctf{m3x1c0_t4c0}', 'tjctf{br4z1l_s4mb4}', 'tjctf{3ngl4nd_t34}', 'tjctf{fr4nc3_b4gu3tt3}', 'tjctf{g3rm4ny_b33r}', 'tjctf{1t4ly_p4st4}', 'tjctf{j4p4n_sush1}', 'tjctf{ch1n4_dr4g0n}']
    return job_flags + emotion_flags + country_flags

def ultimate_confusion_generator():
    element_flags = ['tjctf{f1r3_fl4m3}', 'tjctf{w4t3r_fl0w}', 'tjctf{34rth_s0l1d}', 'tjctf{41r_w1nd}', 'tjctf{1c3_c0ld}', 'tjctf{st34m_h0t}', 'tjctf{l1ght_br1ght}', 'tjctf{d4rkn3ss_bl4ck}', 'tjctf{3n3rgy_p0w3r}', 'tjctf{m4tt3r_4t0m}']
    mythical_flags = ['tjctf{dr4g0n_f1r3}', 'tjctf{un1c0rn_m4g1c}', 'tjctf{ph03n1x_r1s3}', 'tjctf{gr1ff1n_fl1ght}', 'tjctf{k r4k3n_t3nt4cl3}', 'tjctf{m1n0t4ur_l4byrnth}', 'tjctf{p3g4sus_w1ng}', 'tjctf{hydr4_h34d}', 'tjctf{c3nt4ur_h0rs3}', 'tjctf{m3rm41d_0c34n}']
    gem_flags = ['tjctf{d14m0nd_sp4rkl3}', 'tjctf{ruby_r3d}', 'tjctf{s4pph1r3_blu3}', 'tjctf{3m3r4ld_gr33n}', 'tjctf{t0p4z_y3ll0w}', 'tjctf{4m3thyst_purpl3}', 'tjctf{0p4l_r41nb0w}', 'tjctf{p34rl_wh1t3}', 'tjctf{qu4rtz_cl34r}', 'tjctf{0bs1d14n_bl4ck}']
    return element_flags + mythical_flags + gem_flags

def more_distractions():
    alphabet = 'abcdefghijklmnopqrstuvwxyz'
    numbers = list(range(100))
    food_flags = ['tjctf{p1zz4_t1m3}', 'tjctf{burr1t0_b0wl}', 'tjctf{c00k13_m0nst3r}', 'tjctf{c4k3_d4y}', 'tjctf{1c3_cr34m}', 'tjctf{d0nut_h0l3}', 'tjctf{sp4gh3tt1_c0d3}', 'tjctf{t4c0_tu3sd4y}', 'tjctf{s4ndw1ch_4rt}', 'tjctf{s0up_s34s0n}']
    animal_flags = ['tjctf{c4t_l0v3r}', 'tjctf{d0g_w4lk3r}', 'tjctf{b1rd_w4tch3r}', 'tjctf{f1sh_t4nk}', 'tjctf{p4nd4_3y3s}', 'tjctf{t1g3r_str1p3s}', 'tjctf{3l3ph4nt_m3m0ry}', 'tjctf{d0lph1n_3ch0}', 'tjctf{p3ngu1n_w4ddl3}', 'tjctf{m0nk3y_bus1n3ss}']
    weather_flags = ['tjctf{r41ny_d4ys}', 'tjctf{sn0wy_n1ghts}', 'tjctf{sunny_sk13s}', 'tjctf{cl0udy_m0rn1ng}', 'tjctf{st0rmy_s34s}', 'tjctf{w1ndy_h1lls}', 'tjctf{m15ty_m0unt41ns}', 'tjctf{h41l_st0rm}', 'tjctf{thu nd3r_r0ll}', 'tjctf{l1ghtn1ng_str1k3}']
    combined = []
    for letter in alphabet[:5]:
        for number in numbers[:5]:
            combined.append(f'{letter}{number}')
    return combined

def final_distraction():
    fake_secrets = ['tjctf{n0p3_try_4g41n}', 'tjctf{wr0ng_p4th}', 'tjctf{st1ll_l00k1ng}', 'tjctf{k33p_s34rch1ng}', 'tjctf{d34d_3nd}', 'tjctf{f4ls3_h0p3}', 'tjctf{m1sl34d1ng}', 'tjctf{r3d_h3rr1ng}', 'tjctf{w1ld_g00s3}', 'tjctf{bl1nd_4ll3y}']
    tech_flags = ['tjctf{c0d3_n1nj4}', 'tjctf{h4ck_th3_pl4n3t}', 'tjctf{cyb3r_gh0st}', 'tjctf{d1g1t4l_w4rr10r}', 'tjctf{b1n4ry_b34st}', 'tjctf{4lgor1thm_k1ng}', 'tjctf{d4t4_dr4g0n}', 'tjctf{c0mpr3ss10n_k1ng}', 'tjctf{3ncrypt10n_l0rd}', 'tjctf{qu4ntum_cr4ck3r}']
    color_flags = ['tjctf{r3d_4l3rt}', 'tjctf{blu3_scr33n}', 'tjctf{gr33n_c0d3}', 'tjctf{y3ll0w_w4rn1ng}', 'tjctf{purpl3_h4z3}', 'tjctf{0r4ng3_fl4m3}', 'tjctf{p1nk_p4nth3r}', 'tjctf{bl4ck_0ps}', 'tjctf{wh1t3_h4t}', 'tjctf{gr4y_4r34}']
    number_flags = ['tjctf{z3r0_d4y}', 'tjctf{0n3_sh0t}', 'tjctf{tw0_f4ct0r}', 'tjctf{thr33_str1k3s}', 'tjctf{f0ur_tw3nty}', 'tjctf{f1v3_st4rs}', 'tjctf{s1x_s3ns3s}', 'tjctf{s3v3n_s34ls}', 'tjctf{31ght_b1ts}', 'tjctf{n1n3_l1v3s}']
    for secret in fake_secrets:
        processed = secret.encode().decode()
    return 'end of distractions'

OuterLayerクラスのprocess_data関数の一番ネストの深いところにあるフラグで通った。

tjctf{f0ggy_d4ys}

loopy (web)

http://localhost:5000/adminを入力してSubmitすると、以下の結果となる。

Access denied. URL parameter included one or more of the following banned keywords: 127, local, 0.0.0.0, ::1, ffff, 017700000001, [::], 2130706433

16進数表現はブラックリストにないので、以下を入力してSubmitしてみる。

http://0x7F000001:5000/admin

この結果、フラグが表示された。

tjctf{i_l0v3_ssssSsrF_9o4a8}

front-door (web)

https://front-door.tjc.tf/account_creationで適当なユーザ名でアカウントを作成すると、クッキーのtokenに以下が設定されている。

eyJhbGciOiAiQURNSU5IQVNIIiwgInR5cCI6ICJKV1QifQ.eyJ1c2VybmFtZSI6ICJub3JhIiwgInBhc3N3b3JkIjogIm5lY28iLCAiYWRtaW4iOiAiZmFsc2UifQ.JZOAYHBBBBNBDDQABXBFJOABZBLBBSOBVLBWVBQRSJJBOJYXDQZBEIRQBSOOFFWB

他のユーザ名でアカウントを作成すると、クッキーのtokenに以下が設定されている。

eyJhbGciOiAiQURNSU5IQVNIIiwgInR5cCI6ICJKV1QifQ.eyJ1c2VybmFtZSI6ICJob2dlIiwgInBhc3N3b3JkIjogImZ1Z2EiLCAiYWRtaW4iOiAiZmFsc2UifQ.JZOAYHBBBBNBDDQABXBFJOABZBLBBSOBVLBWVBQRSJJBOJYXDQZBEIRQBSOOFFWB

signature部分はアカウントの情報を変更しても変わらない。

$ echo eyJhbGciOiAiQURNSU5IQVNIIiwgInR5cCI6ICJKV1QifQ | base64 -d
{"alg": "ADMINHASH", "typ": "JWT"}
$ echo eyJ1c2VybmFtZSI6ICJob2dlIiwgInBhc3N3b3JkIjogImZ1Z2EiLCAiYWRtaW4iOiAiZmFsc2UifQ | base64 -d
{"username": "hoge", "password": "fuga", "admin": "false"}

adminをtrueにして、念のため、ユーザ名等もadminにした情報に変更する。

$ echo -n '{"username": "admin", "password": "admin", "admin": "true"}' | base64
eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwYXNzd29yZCI6ICJhZG1pbiIsICJhZG1pbiI6ICJ0cnVl
In0=

クッキーのtokenに以下を設定し、リロードする。

eyJhbGciOiAiQURNSU5IQVNIIiwgInR5cCI6ICJKV1QifQ.eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwYXNzd29yZCI6ICJhZG1pbiIsICJhZG1pbiI6ICJ0cnVlIn0.JZOAYHBBBBNBDDQABXBFJOABZBLBBSOBVLBWVBQRSJJBOJYXDQZBEIRQBSOOFFWB

TODOボタンを押すと、以下のように表示された。

if you want to read my todo list you must be me and understand my secret code!
[108, 67, 82, 10, 77, 70, 67, 94, 73, 66, 79, 89]
[107, 78, 92, 79, 88, 94, 67, 89, 79, 10, 73, 69, 71, 90, 75, 68, 83]
[105, 88, 79, 75, 94, 79, 10, 8, 72, 95, 89, 67, 68, 79, 89, 89, 117, 89, 79, 73, 88, 79, 94, 89, 8, 10, 90, 75, 77, 79, 10, 7, 7, 10, 71, 75, 78, 79, 10, 67, 94, 10, 72, 95, 94, 10, 68, 69, 10, 72, 95, 94, 94, 69, 68, 10, 94, 69, 10, 75, 73, 73, 79, 89, 89, 10, 83, 79, 94]
[126, 75, 65, 79, 10, 69, 92, 79, 88, 10, 94, 66, 79, 10, 93, 69, 88, 70, 78, 10, 7, 7, 10, 75, 70, 71, 69, 89, 94, 10, 78, 69, 68, 79]

https://front-door.tjc.tf/robots.txtにアクセスすると、以下のように書いてある。

User-agent: * 
Disallow: 

# Gonna jot the encryption scheme I use down for later -The Incredible Admin
#
# def encrypt(inp):
#   enc = [13]
#   for i in range(len(inp)):
#     enc.append(ord(inp[i]) ^ 42)
#   return enc[1:]

この暗号アルゴリズムと推測し、表示されたコードを復号する。

>>> s = [108, 67, 82, 10, 77, 70, 67, 94, 73, 66, 79, 89]
>>> ''.join([chr(c ^ 42) for c in s])
'Fix glitches'
>>> s = [107, 78, 92, 79, 88, 94, 67, 89, 79, 10, 73, 69, 71, 90, 75, 68, 83]
>>> ''.join([chr(c ^ 42) for c in s])
'Advertise company'
>>> s = [105, 88, 79, 75, 94, 79, 10, 8, 72, 95, 89, 67, 68, 79, 89, 89, 117, 89, 79, 73, 88, 79, 94, 89, 8, 10, 90, 75, 77, 79, 10, 7, 7, 10, 71, 75, 78, 79, 10, 67, 94, 10, 72, 95, 94, 10, 68, 69, 10, 72, 95, 94, 94, 69, 68, 10, 94, 69, 10, 75, 73, 73, 79, 89, 89, 10, 83, 79, 94]
>>> ''.join([chr(c ^ 42) for c in s])
'Create "business_secrets" page -- made it but no button to access yet'
>>> s = [126, 75, 65, 79, 10, 69, 92, 79, 88, 10, 94, 66, 79, 10, 93, 69, 88, 70, 78, 10, 7, 7, 10, 75, 70, 71, 69, 89, 94, 10, 78, 69, 68, 79]
>>> ''.join([chr(c ^ 42) for c in s])
'Take over the world -- almost done'

business_secretsページが作成されているが、アクセスするボタンは用意されていないということらしい。
https://front-door.tjc.tf/business_secretsにアクセスしたら、フラグが表示された。

tjctf{buy_h1gh_s3l1_l0w}

hidden-message (forensics)

添付されているpng画像のLSBに文字情報がないか確認してみる。

$ zsteg suspicious.png 
imagedata           .. file: Targa image data - Map 1024 x 1023 x 1 +256 +259 "\002\003"
b1,rgb,lsb,xy       .. text: "tjctf{steganography_is_fun}###END###"
b2,g,lsb,xy         .. text: ["U" repeated 25 times]
b2,g,msb,xy         .. text: ["U" repeated 25 times]
b4,g,lsb,xy         .. file: 0420 Alliant virtual executable not stripped
b4,g,msb,xy         .. text: ["D" repeated 50 times]
b4,bgr,lsb,xy       .. file: 0421 Alliant compact executable
tjctf{steganography_is_fun}

deep-layers (forensics)

$ exiftool chall.png                     
ExifTool Version Number         : 13.00
File Name                       : chall.png
Directory                       : .
File Size                       : 370 bytes
File Modification Date/Time     : 2025:06:07 06:36:52+09:00
File Access Date/Time           : 2025:06:07 09:54:11+09:00
File Inode Change Date/Time     : 2025:06:07 06:36:52+09:00
File Permissions                : -rwxrwxrwx
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 1
Image Height                    : 1
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Password                        : cDBseWdsMHRwM3NzdzByZA==
Warning                         : [minor] Trailer data after PNG IEND chunk
Image Size                      : 1x1
Megapixels                      : 0.000001

Passwordにbase64文字列が設定されているので、デコードする。

$ echo cDBseWdsMHRwM3NzdzByZA== | base64 -d
p0lygl0tp3ssw0rd

$ binwalk chall.png

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image, 1 x 1, 8-bit/color RGBA, non-interlaced
90            0x5A            Zlib compressed data, default compression
119           0x77            Zip archive data, encrypted at least v1.0 to extract, compressed size: 67, uncompressed size: 55, name: secret.gz
348           0x15C           End of Zip archive, footer length: 22

pngの後ろにzipがあるので、切り出す。

$ dd bs=1 skip=119 if=chall.png of=chall.zip
251+0 records in
251+0 records out
251 bytes copied, 0.0263772 s, 9.5 kB/s

chall.zipを先ほどのパスワードで解凍し、内容を確認する。

$ unzip chall.zip
Archive:  chall.zip
[chall.zip] secret.gz password: 
 extracting: secret.gz
$ gzip -d secret.gz                                    
gzip: secret: Value too large for defined data type
$ cat secret                             
tjctf{p0lygl0t_r3bb1t_h0l3}
tjctf{p0lygl0t_r3bb1t_h0l3}

footprint (forensics)

.DS_Storeから情報を抽出する。https://github.com/gehaxelt/Python-dsstore/のツールを使う。

$ git clone https://github.com/gehaxelt/Python-dsstore/
Cloning into 'Python-dsstore'...
remote: Enumerating objects: 38, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 38 (delta 1), reused 1 (delta 0), pack-reused 30 (from 1)
Receiving objects: 100% (38/38), 12.33 KiB | 2.05 MiB/s, done.
Resolving deltas: 100% (8/8), done.
$ cd Python-dsstore 
$ python3 main.py ../.DS_Store    
Count:  100
-FumtF3yx-kSP11OD8mFPA
0wnNJd_pKKNtfhG-HL8iJw
1VmhSaBo9ymK5dUhB3cPEQ
1zp7dw6eF3co0VaPDKhUag
27bCy1Bt-9nnLG4W8oxkNA
4vsxjPs-c9hBNmmaE8HJ8Q
5rc69mw3DNjUvLolTrP3ew
6zed3-nVA008etbNxGTNEQ
78ICY1U_sI9qqF6vv97RhA
7AJTVqVtlVelnulrRMUCaQ
7P7nopvmQj2usona47YjSA
7v9Vn8ci1_C1tyjKpOrDjA
9lZ0k-7YFRkQu1QhA-d-DA
_KxyLnw2LZxOc0Tk9U0cig
A69F-dk9M1nQFfzi06gLPw
abvFjWgNkHKQYbaMyCjdlw
AFEvM2adjTHq0E8noIE0kw
aNHzuom-c5UIGbW5ceGc9g
aNPc9lG0gpLnRvGI2JPQMA
aXNfdXNlZnVsP30gICAgIA
b-PeUWwmlHzmh613ikEFWw
b0HMxEHbs7pA9uHtxRPPTQ
Bg9XKyNnYRSpUIYeK_2knA
CE4CzpPMjNHuYeLi2dLNHg
CPyvVdX_ynvUTdxXWYr7Mw
D0FvxLGtoba2wKJQQEjUKA
D3LWCMzIQyc12SQUw5uDnw
DFhc0ROB762T-pZvtdPFqA
dGpjdGZ7ZHNfc3RvcmVfIA
dtiqv7O15HVT3k4aL1sTDA
Dwv-qkn0QQLu8RlhwUNNeA
e1xUP4GStK_lTs2W6gEkPA
E6IuP9ouzpQDYXSNNdSyFw
E8tEXrVCNAqQmSji8cDWqQ
eNSX5RnqD95dCp7TNI2zOg
eSRLCk2xqnpx1htFqlBAvw
f0wkaX7NmMMATw1grKXIyw
FsmKr0zCKawO9TVNgKkVvA
FxWYnzxWEHMQGNLR_7uXRw
g8azPwD-y-_2VU-dmRK7IA
GQxeqKQR4yLZIz889h8awQ
iHFj7XDSIesD-TJ-aSiTyA
jg1spCuL4Vix9bgpToP5Hg
JuARuIvr7ZvOOpeJ9LTOvA
kAbgNCWwQGUZWFktiIIHeA
kfAVM8pkh2jSSeuP0uWGog
LWUOeeOqK7mlQOTJmSmwVA
LxtZhpBRiU8PSE4eXQZV0w
meMIlojtiqbBuHOGDYud_Q
mLO_JmXEG0tcWougAWQ6Qw
MnbgbJqhwFXlh_kKGIYLJQ
N3lsvxkjSRnwIfz3Z7C5uw
n8KZj1W3tx2WIXg8HqtF3g
niZvI6zZ8yzoSl17d963mQ
Ns2Tpp4fQxW5zhYLLGIVdA
NUhhZkAZsgdvPVVF3KzZpA
p7s4fwmK70UDkM_ApzmX3A
PgbanHSdf0H3qDXVUrVAaA
PGjsQh7wml99RXiA-gta6Q
pJ_ampVpcIVGVZErPVONDQ
pm_TeJmHmlL-5Mdv3R1YoA
pMOW9YUc2Zrd-6B5G3-NSQ
Po5jOoQ4HUssvLHuCmDj5g
pOGlM0FXA5tvruLyZ5AVRA
pPd8nFycxgq3SD67StjdCQ
PqYOp2_ps2oErxR5U5uSXA
PrceMk6k6v8-gPc6YUfuvQ
prsJSTpQJJX5eKJQF3akDg
PuAZy-41HFCOsKTCZkwDBw
QJWXuKXCsnG2mjGYYbyoaA
QLpbpFhcDb2oapdj3Ygutg
Qs13PznBoQJC9yjgWm-clQ
QzopSLFcXVCF5sII8C8jJA
r4NAxKJ_RMhOLA468CAuLQ
rhnhvlSsjRdNv35ZYwqSMg
rkFwpUQRoffUQmfnqKFCNg
sOcYOUIJIYjTYFNud5htCA
sPEu3qqkuJDSVB6LZ0x82w
tCSdwHZsNBvNS3h4qih6tA
TibD2LWT-7Xua5Wmivc-6A
TM0MhKzOCDKMYolGyoYc3g
TxlnTHhAAyM7wIn3PGdLEg
Ur4ktp41Mmf49_FANNugHQ
uvIo5poX5D2dGKif1JiWIA
uXcTlwm5yaS69kQd0YlYgQ
V2XHU0KaptQFjruBnOeYJw
V3JQfigsgWSgJ2bu8IuPOQ
vNgO1oK2Ft-Q_OVtcjk7og
VRU6bDaPkqPqFxsfBjrPQA
VVa_NUjLLaMsO2_Jwko-SA
w1oE_3GO5OTRADuEQ9Pqnw
W62TUuIfC_ma91QdMu4ISA
wzeP722FHEtirWHFJrgP2A
xcTHd2ZbYtO9LQ2fmaQo1Q
XfjqZgZvquXzdnfbcMKQMA
Xnji8EXzCRLmKJvoAkZftA
yfmS9_zOUIcxfSY-obWMhg
ylFen4T5uMeqvJC6p8dfkA
yx7GMqzc5YejMzOO5F087g
Z3cpkGAUMlLgzQctzCo2Zg

各行をbase64デコードし、フラグの断片を抽出したものを構成する。

#!/usr/bin/env python3
from base64 import *

def is_printable(s):
    for c in s:
        if c < 32 or c > 126:
            return False
    return True

files = '''-FumtF3yx-kSP11OD8mFPA
0wnNJd_pKKNtfhG-HL8iJw
1VmhSaBo9ymK5dUhB3cPEQ
1zp7dw6eF3co0VaPDKhUag
27bCy1Bt-9nnLG4W8oxkNA
4vsxjPs-c9hBNmmaE8HJ8Q
5rc69mw3DNjUvLolTrP3ew
6zed3-nVA008etbNxGTNEQ
78ICY1U_sI9qqF6vv97RhA
7AJTVqVtlVelnulrRMUCaQ
7P7nopvmQj2usona47YjSA
7v9Vn8ci1_C1tyjKpOrDjA
9lZ0k-7YFRkQu1QhA-d-DA
_KxyLnw2LZxOc0Tk9U0cig
A69F-dk9M1nQFfzi06gLPw
abvFjWgNkHKQYbaMyCjdlw
AFEvM2adjTHq0E8noIE0kw
aNHzuom-c5UIGbW5ceGc9g
aNPc9lG0gpLnRvGI2JPQMA
aXNfdXNlZnVsP30gICAgIA
b-PeUWwmlHzmh613ikEFWw
b0HMxEHbs7pA9uHtxRPPTQ
Bg9XKyNnYRSpUIYeK_2knA
CE4CzpPMjNHuYeLi2dLNHg
CPyvVdX_ynvUTdxXWYr7Mw
D0FvxLGtoba2wKJQQEjUKA
D3LWCMzIQyc12SQUw5uDnw
DFhc0ROB762T-pZvtdPFqA
dGpjdGZ7ZHNfc3RvcmVfIA
dtiqv7O15HVT3k4aL1sTDA
Dwv-qkn0QQLu8RlhwUNNeA
e1xUP4GStK_lTs2W6gEkPA
E6IuP9ouzpQDYXSNNdSyFw
E8tEXrVCNAqQmSji8cDWqQ
eNSX5RnqD95dCp7TNI2zOg
eSRLCk2xqnpx1htFqlBAvw
f0wkaX7NmMMATw1grKXIyw
FsmKr0zCKawO9TVNgKkVvA
FxWYnzxWEHMQGNLR_7uXRw
g8azPwD-y-_2VU-dmRK7IA
GQxeqKQR4yLZIz889h8awQ
iHFj7XDSIesD-TJ-aSiTyA
jg1spCuL4Vix9bgpToP5Hg
JuARuIvr7ZvOOpeJ9LTOvA
kAbgNCWwQGUZWFktiIIHeA
kfAVM8pkh2jSSeuP0uWGog
LWUOeeOqK7mlQOTJmSmwVA
LxtZhpBRiU8PSE4eXQZV0w
meMIlojtiqbBuHOGDYud_Q
mLO_JmXEG0tcWougAWQ6Qw
MnbgbJqhwFXlh_kKGIYLJQ
N3lsvxkjSRnwIfz3Z7C5uw
n8KZj1W3tx2WIXg8HqtF3g
niZvI6zZ8yzoSl17d963mQ
Ns2Tpp4fQxW5zhYLLGIVdA
NUhhZkAZsgdvPVVF3KzZpA
p7s4fwmK70UDkM_ApzmX3A
PgbanHSdf0H3qDXVUrVAaA
PGjsQh7wml99RXiA-gta6Q
pJ_ampVpcIVGVZErPVONDQ
pm_TeJmHmlL-5Mdv3R1YoA
pMOW9YUc2Zrd-6B5G3-NSQ
Po5jOoQ4HUssvLHuCmDj5g
pOGlM0FXA5tvruLyZ5AVRA
pPd8nFycxgq3SD67StjdCQ
PqYOp2_ps2oErxR5U5uSXA
PrceMk6k6v8-gPc6YUfuvQ
prsJSTpQJJX5eKJQF3akDg
PuAZy-41HFCOsKTCZkwDBw
QJWXuKXCsnG2mjGYYbyoaA
QLpbpFhcDb2oapdj3Ygutg
Qs13PznBoQJC9yjgWm-clQ
QzopSLFcXVCF5sII8C8jJA
r4NAxKJ_RMhOLA468CAuLQ
rhnhvlSsjRdNv35ZYwqSMg
rkFwpUQRoffUQmfnqKFCNg
sOcYOUIJIYjTYFNud5htCA
sPEu3qqkuJDSVB6LZ0x82w
tCSdwHZsNBvNS3h4qih6tA
TibD2LWT-7Xua5Wmivc-6A
TM0MhKzOCDKMYolGyoYc3g
TxlnTHhAAyM7wIn3PGdLEg
Ur4ktp41Mmf49_FANNugHQ
uvIo5poX5D2dGKif1JiWIA
uXcTlwm5yaS69kQd0YlYgQ
V2XHU0KaptQFjruBnOeYJw
V3JQfigsgWSgJ2bu8IuPOQ
vNgO1oK2Ft-Q_OVtcjk7og
VRU6bDaPkqPqFxsfBjrPQA
VVa_NUjLLaMsO2_Jwko-SA
w1oE_3GO5OTRADuEQ9Pqnw
W62TUuIfC_ma91QdMu4ISA
wzeP722FHEtirWHFJrgP2A
xcTHd2ZbYtO9LQ2fmaQo1Q
XfjqZgZvquXzdnfbcMKQMA
Xnji8EXzCRLmKJvoAkZftA
yfmS9_zOUIcxfSY-obWMhg
ylFen4T5uMeqvJC6p8dfkA
yx7GMqzc5YejMzOO5F087g
Z3cpkGAUMlLgzQctzCo2Zg'''

flags = []
for file in files.splitlines():
    while True:
        if len(file) % 4 == 0:
            break
        file += '='
    d = urlsafe_b64decode(file)
    if is_printable(d):
        flags.append(d.decode().strip())

assert len(flags) == 2
assert '}' in flags[0]
assert 'tjctf{' in flags[1]

flag = flags[1] + flags[0]
print(flag)
tjctf{ds_store_is_useful?}

packet-palette (forensics)

パケットを見ていくと、No.2~No.22のTCP payloadの一部を結合するとPNGデータになりそうであることがわかった。結合して画像を復元してみる。

#!/usr/bin/env python3
from scapy.all import *

packets = rdpcap('chall.pcapng')

png = b''
for i in range(len(packets)):
    data = packets[i][Raw].load
    head = data[:12]
    assert head[:4] == b'USBI'
    if data[4] == 0:
        assert data[5] == i - 1
        png += data[12:]

with open('flag.png', 'wb') as f:
    f.write(png)

復元した画像にフラグが書いてあった。

tjctf{usb1p_f13g_1ns1d3_3_pr0t0c0l}

bacon-bits (crypto)

暗号化処理の概要は以下の通り。

・flag: フラグ
・text: 未知固定文字列
・baconian: アルファベットとベコニアン暗号の対応表
・text: textの各文字の配列
・ciphertext = ''
・flagの各文字lとそのインデックスiについて以下を実行
 ・lがアルファベットでない場合何もしない
 ・change = baconian[l]
 ・text[i*5:(i+1)*5]の各文字ltとそのインデックスixについて、change[ix]が"1"の場合ltを大文字で、"0"の場合ltを小文字にして、ciphertextに結合
・out.txtにciphertextのASCIIコードで13引いたものの文字を連結したものを出力

逆算していけば、フラグを復号できる。

#!/usr/bin/env python3
baconian = {
    'a': '00000',    'b': '00001',
    'c': '00010',    'd': '00011',
    'e': '00100',    'f': '00101',
    'g': '00110',    'h': '00111',
    'i': '01000',    'j': '01000',
    'k': '01001',    'l': '01010',
    'm': '01011',    'n': '01100',
    'o': '01101',    'p': '01110',
    'q': '01111',    'r': '10000',
    's': '10001',    't': '10010',
    'u': '10011',    'v': '10011',
    'w': '10100',    'x': '10101',
    'y': '10110',    'z': '10111'}

with open('out.txt', 'r') as f:
    ct = f.read()

ciphertext = ''.join([chr(ord(i) + 13) for i in ct])
baconian_ct = ''
for c in ciphertext:
    if c.isupper():
        baconian_ct += '1'
    else:
        baconian_ct += '0'

flag = ''
for i in range(0, len(baconian_ct), 5):
    change = baconian_ct[i:i+5]
    for k, v in baconian.items():
        if v == change:
            flag += k
            break

print(flag)

復号結果は以下の通り。

tictfoinkooinkoooinkooooink

フラグの形式に整形する。

tjctf{oinkooinkoooinkooooink}

alchemist-recipe (crypto)

暗号処理の概要は以下の通り。

・SNEEZE_FORK = "AurumPotabileEtChymicumSecretum"
・WUMBLE_BAG = 8
・flag_content: flag.txtの内容
・jellybean = glorbulate_sprockets_for_bamboozle(SNEEZE_FORK)
 ※固定データ
・encrypted_recipe = snizzle_bytegum(flag_content, jellybean)
 ・bubbles: flag_contentの8バイトパディング
 ・glomp = b''
 ・bunnlesの8バイトブロックsplinterごとに以下を実行
  ・zap = scrungle_crank(splinter, jellybean)
   ・zonked: splinterの各値をjellybean['drizzle'][x]に置換
   ・quix = jellybean['twizzle'](16個の既知固定数値から構成する配列)
   ・splatted: zonkedとquixの8個の要素のXOR
   ・wiggle = jellybean['flibber'](8個の既知固定数値から構成する配列)
   ・waggly: iが0~7の(wiggle[i], i)をソートしたもの
   ・zort: wagglyの各要素の2つ目の値のみを配列にしたもの
   ・plunk = [0] * 8
   ・0~7のyについて以下を実行
    ・x = zort[y]
    ・plunk[y] = splatted[x]
   ・plunkのバイト文字列を返却
  ・glompにzapを結合
 ・glompを返却
・encrypted.txtにencrypted_recipeの16進文字列化したものを出力

出力結果の16進数文字列をhexデコードし、8バイトごとに分け、以下のことに着目して復号する。

・jellybeanは既知固定データのため、quix、wiggleは既知固定データである。
・wiggleが既知固定データであるため、waggly、zortは固定データである。
#!/usr/bin/env python3
import hashlib
from Crypto.Util.Padding import unpad

SNEEZE_FORK = "AurumPotabileEtChymicumSecretum"
WUMBLE_BAG = 8

def glorbulate_sprockets_for_bamboozle(blorbo):
    zing = {}
    yarp = hashlib.sha256(blorbo.encode()).digest() 
    zing['flibber'] = list(yarp[:WUMBLE_BAG])
    zing['twizzle'] = list(yarp[WUMBLE_BAG:WUMBLE_BAG+16])
    glimbo = list(yarp[WUMBLE_BAG+16:])
    snorb = list(range(256))
    sploop = 0
    for _ in range(256): 
        for z in glimbo:
            wob = (sploop + z) % 256
            snorb[sploop], snorb[wob] = snorb[wob], snorb[sploop]
            sploop = (sploop + 1) % 256
    zing['drizzle'] = snorb
    return zing

jellybean = glorbulate_sprockets_for_bamboozle(SNEEZE_FORK)
quix = jellybean['twizzle']
wiggle = jellybean['flibber']
waggly = sorted([(wiggle[i], i) for i in range(WUMBLE_BAG)])
zort = [oof for _, oof in waggly]

with open('encrypted.txt', 'r') as f:
    encrypted_recipe = bytes.fromhex(f.read())

flag = b''
for i in range(0, len(encrypted_recipe), WUMBLE_BAG):
    zap = encrypted_recipe[i:i+WUMBLE_BAG]
    splatted = [0] * 8
    for y in range(WUMBLE_BAG):
        x = zort[y]
        splatted[x] = zap[y]
    zonked = bytes([splatted[i] ^ quix[i] for i in range(WUMBLE_BAG)])
    splinter = bytes([jellybean['drizzle'].index(x) for x in zonked])
    flag += splinter

flag = unpad(flag, WUMBLE_BAG).decode()
print(flag)
tjctf{thank_you_for_making_me_normal_again_yay}

theartofwar (crypto)

暗号処理の概要は以下の通り。

・flag: flag.txtの内容
・m: flagを数値化したもの
・e: 8ビット素数
・eを出力
・e回以下繰り返し
 ・n = generate_key()
 ・c = pow(m, e, n)
 ・nを出力
 ・cを出力

Hastad's Broadcast Attackで復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *
from sympy.ntheory.modular import crt
from gmpy2 import iroot

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

e = int(params[0].split(' ')[-1])

ns = []
cs = []
for i in range(e):
    n = int(params[i * 2 + 1].split(' ')[-1])
    c = int(params[i * 2 + 2].split(' ')[-1])
    ns.append(n)
    cs.append(c)

me, _ = crt(ns, cs)
m, success = iroot(me, e)
assert success
flag = long_to_bytes(m).decode()
print(flag)
tjctf{the_greatest_victory_is_that_which_require_no_battle}

seeds (crypto)

サーバの処理概要は以下の通り。

・randgen = RandomGenerator()
 ・randgen.seed: 現在時刻を文字表記したもの(例:Sat Jun  7 16:07:33 2025)
 ・randgen.seed: randgen.seedを数値化したもの
 ・randgen.m = 2 ** 32
 ・randgen.a = 157
 ・randgen.c = 1
・cipher = AES.new(randgen.randbytes(32), AES.MODE_ECB)
 ・randgen.randbytes(32) = randgen.randint(32 * 8).to_bytes(len, "big")
  ・randgen.randint(32 * 8)
   ・randgen.seed = (randgen.a * randgen.seed + randgen.c) % randgen.m
   ・result: randgen.seedを4バイト文字列化したもの
   ・resultの長さが32になるまで、以下繰り返し
    ・randgen.seed = (randgen.a * randgen.seed + randgen.c) % randgen.m
    ・resultにrandgen.seedを4バイト文字列化したものを結合
   ・resultを数値化したものを返却
・flag: flag.txtの内容
・ciphertext: flagをパディングし、cipherオブジェクトで暗号化したもの
・ciphertextを表示
・以下繰り返し
 ・plaintext: 入力
 ・plaintextが"quit"の場合、繰り返し終了
 ・cipher = AES.new(randgen.randbytes(32), AES.MODE_ECB)
 ・ciphertext: plaintextをパディングし、cipherオブジェクトで暗号化したもの
 ・ciphertextを表示

最初のseedが決まれば、暗号鍵は決まる。またタイムゾーンはESTのようなので、夏時間ではJSTの13時間前を示すことに注意すれば、簡単に復号できる。

#!/usr/bin/env python3
import socket
import time
from datetime import datetime
from zoneinfo import ZoneInfo
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

class RandomGenerator:
    def __init__(self, seed = None, modulus = 2 ** 32, multiplier = 157, increment = 1):
        if seed is None: 
            seed = time.asctime()
        if type(seed) is int: 
            self.seed = seed
        if type(seed) is str: 
            self.seed = int.from_bytes(seed.encode(), "big")
        if type(seed) is bytes: 
            self.seed = int.from_bytes(seed, "big")
        self.m = modulus
        self.a = multiplier
        self.c = increment

    def randint(self, bits: int):
        self.seed = (self.a * self.seed + self.c) % self.m
        result = self.seed.to_bytes(4, "big")
        while len(result) < bits // 8:
            self.seed = (self.a * self.seed + self.c) % self.m
            result += self.seed.to_bytes(4, "big")
        return int.from_bytes(result, "big") % (2 ** bits)

    def randbytes(self, len: int):
        return self.randint(len * 8).to_bytes(len, "big")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('tjc.tf', 31493))

data = recvuntil(s, b'\n').rstrip()
print(data)

dt = datetime.now(ZoneInfo('America/New_York'))
seed = time.asctime(dt.timetuple())
randgen = RandomGenerator(seed=seed)

data = recvuntil(s, b'\n').rstrip()
print(data)
enc_flag = eval(data.split(' = ')[1])

cipher = AES.new(randgen.randbytes(32), AES.MODE_ECB)
flag = unpad(cipher.decrypt(enc_flag), AES.block_size).decode()
print(flag)

実行結果は以下の通り。

Welcome to the AES Oracle
ciphertext = b'I<B\x8f7\x1a\x9d\xba\xcb=Dz8\x97\xe9c\xb7\xaf\x15\x01\xf4\xd9\xd9\xc2\x83jm\x1a\xa2\xda\x10\xb5'
tjctf{h4rv3st_t1me}
tjctf{h4rv3st_t1me}

close-secrets (crypto)

暗号処理の概要は以下の通り。

・flag_plaintext: flag.txtの内容
・flag_bytes: flag_plaintextのバイト文字列エンコーディング
・p, g, u, v, shared_key = generate_dh_key()
 ・p: 1024ビット素数
 ・g: 1024ビット素数
 ・a: p - 10以上p以下の整数
 ・b: g - 10以上g以下の整数
 ・u = pow(g, a, p)
 ・v = pow(g, b, p)
 ・key = pow(v, a, p)
 ・b_key = pow(u, b, p)
 ・p, g, u, v, keyを返却
・xor_key_str = hashlib.sha256(str(shared_key).encode()).hexdigest()
・xor_key_bytes: xor_key_strのバイト文字列エンコーディング
・intermediate_ords = dynamic_xor_encrypt(flag_bytes, xor_key_bytes)
 ・flag_bytesを逆順にしたものとxor_key_bytesの繰り返しとXORしたものを返却
・final_cipher = encrypt_outer(intermediate_ords, shared_key)
 ・cipher = []
 ・key_offset = shared_key % 256
 ・intermediate_ordsの各値valに対して以下を実行
  ・cipherにvalにkey_offsetをプラスしkeyを掛けたものを追加
 ・cipherを返却
・p, g, u, vをparams.txtに書き込み
・final_cipherをenc_flagに書き込み

a, bの範囲が狭いため、ブルートフォースでa, bを求めることができ、shared_keyがわかる。shared_keyがわかったら逆算していけば、フラグが得られる。

#!/usr/bin/env python3
import hashlib

def decrypt_outer(ciphertext_ords, key):
    plain = []
    key_offset = key % 256
    for val in ciphertext_ords:
        if not isinstance(val, int):
            raise TypeError
        assert val % key == 0
        plain.append(val // key - key_offset)
    return plain

def dynamic_xor_decrypt(ciphertext_bytes, text_key_bytes):
    decrypted_ords = []
    key_length = len(text_key_bytes)
    if not isinstance(ciphertext_bytes, bytes):
        raise TypeError
    for i, byte_val in enumerate(ciphertext_bytes):
        key_byte = text_key_bytes[i % key_length]
        decrypted_ords.append(byte_val ^ key_byte)
    return bytes(decrypted_ords[::-1])

with open('params.txt', 'r') as f:
    params = f.read().splitlines()

p = int(params[0].split(' ')[-1])
g = int(params[1].split(' ')[-1])
u = int(params[2].split(' ')[-1])
v = int(params[3].split(' ')[-1])

for a in range(p - 10, p + 1):
    if pow(g, a, p) == u:
        break

shared_key = pow(v, a, p)

with open('enc_flag', 'r') as f:
    final_cipher = eval(f.read())

intermediate_ords = decrypt_outer(final_cipher, shared_key)
intermediate_bytes = bytes(intermediate_ords)

xor_key_str = hashlib.sha256(str(shared_key).encode()).hexdigest()
xor_key_bytes = xor_key_str.encode('utf-8')

flag_bytes = dynamic_xor_decrypt(intermediate_bytes, xor_key_bytes)
flag = flag_bytes.decode()
print(flag)
tjctf{sm4ll_r4ng3_sh0rt_s3cr3t}

double-trouble (crypto)

暗号処理の概要は以下の通り。

・k1, k2 = gen()
 ・myrandom: 42をシードとした乱数設定
 ・k1: ランダム8バイト文字列
 ・choices: ランダム6バイト文字列のリスト化したもの
 ・k2 = b''
 ・8回以下繰り返し
  ・k2にchoicesのランダム0以上3以下のインデックスの値をバイト文字列として追加
 ・k1, k2を返却
・k3, k4 = gen()
・pt = b"example"
・ct1 = enc(pt, k1, k2, k3, k4)
 ・key1 = k1 + k2
 ・ct1: ptをパディングしてkey1でAES ECBモード暗号化したもの
 ・key2 = k4 + k3
 ・ct2 = ct1をパディングしてkey1でAES ECBモード暗号化したもの
・ct1を16進数表記でout.txtに書き込み
・ct2 = enc(flag, k1, k2, k3, k4)
・ct2を16進数表記でout.txtに書き込み

gen関数のk1は固定の8バイト文字列。またgen関数のchoicesは固定でインデックス0~3は4つの値しかない。k2はそれが8バイトであるため、全部で65536パターンしかない。

enc関数では2つの鍵それぞれが65536パターンあり、二重に暗号化している。平文と暗号文の組が1つわかっているので、平文を1回暗号化したものと暗号文を1回復号したものを突き合わせ、一致するものを探す。
あとはそれを使って、フラグを復号する。

#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import random
import itertools

with open("out.txt", "r") as f:
    params = f.read().splitlines()

pt = b"example"
ct = bytes.fromhex(params[0])
ct_flag = bytes.fromhex(params[1])

myrandom = random.Random(42)
k1 = myrandom.randbytes(8)
choices = list(myrandom.randbytes(6))[:4]
k3 = k1

key1_list = []
ct1_list = []
for x in itertools.product(choices, repeat=8):
    k2 = bytes(x)
    key1 = k1 + k2
    cipher = AES.new(key1, mode=AES.MODE_ECB)
    ct1 = cipher.encrypt(pad(pt, 16))
    key1_list.append(key1)
    ct1_list.append(ct1)

for x in itertools.product(choices, repeat=8):
    k4 = bytes(x)
    key2 = k4 + k3
    cipher = AES.new(key2, mode=AES.MODE_ECB)
    ct1 = cipher.decrypt(ct)
    if ct1 in ct1_list:
        index = ct1_list.index(ct1)
        key1 = key1_list[index]
        break

print('[+] key1:', key1)
print('[+] key2:', key2)

cipher = AES.new(key2, mode=AES.MODE_ECB)
ct1 = cipher.decrypt(ct_flag)
cipher = AES.new(key1, mode=AES.MODE_ECB)
flag = unpad(cipher.decrypt(ct1), 16).decode()
print('[*] flag:', flag)

実行結果は以下の通り。

[+] key1: b'\x9dy\xb1\xa3\x7f1\x80\x1c\x1a\x1a\x1a\x1agg\x1a\x06'
[+] key2: b'\x1ag\x1a\x1a\x06\xd1\x1a\x1a\x9dy\xb1\xa3\x7f1\x80\x1c'
[*] flag: tjctf{m33t_in_th3_middl3}
tjctf{m33t_in_th3_middl3}

survey (misc)

アンケートに答えたら、フラグが表示された。

tjctf{and_we_say_bye_bye_until_next_year}



以上の内容はhttps://yocchin.hatenablog.com/entry/2025/06/10/075510より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14