$ cat writeup.md…
$ cat writeup.md…
TJCTF 2026
Task: Godot 4.6 platformer game exported to WebAssembly with a hidden flag scene placed above the viewport, reachable only via an unbound 'mega_jump' action. Solution: Extract and decompile the .pck package with GDRE Tools, analyze the flag scene (f.tscn) containing 56 ColorRect nodes, render their coordinates to reveal the flag text.
Sometimes, it takes a broken pair of legs to accomplish your goals. URL: https://jumper.tjc.tf/
A Godot 4.6 platformer game exported to WebAssembly. The player controls a character that can move left/right and jump. The goal is to find the hidden flag, which is placed above the normal viewport and unreachable through normal gameplay because the required "mega_jump" action has no key bound to it.
The target URL serves a Godot 4.6 game via WebAssembly. The HTML page loads three key files:
jumper.pck — Godot package file (1.2 MB) containing all game assetsjumper.wasm — WebAssembly binary (Godot runtime)jumper.js — JavaScript glue codeDownloaded jumper.pck and identified it as a Godot 4 pack (version 3 format) with GDPC magic bytes. The package contains Zstandard-compressed GDScript bytecode (GDSC format) with XOR-encoded (0xb6b6b6b6) UTF-32LE identifiers.
Initial manual parsing with a custom Python script (extract_pck.py) successfully listed all 23 files and extracted raw data, but the GDScript files were in compiled bytecode format. Used GDRE Tools (gdsdecomp) v2.5.0-beta.5 with --headless --recover CLI to fully decompile all GDScript source and scene files.
player.gd — Standard platformer controller (CharacterBody2D):
const SPEED = 200 const JUMP_VELOCITY = 370 const GRAVITY = 10 func _physics_process(_delta): if Input.is_action_just_pressed("mega_jump") and is_on_floor(): velocity.y -= 500 # Very powerful jump if jump_input and can_jump: velocity.y = -JUMP_VELOCITY # Regular jump (-370)
Two jump types exist:
velocity.y = -370 — standard heightvelocity.y -= 500 — much more powerful, only works on floorproject.godot — Input action mappings:
mega_jump={ "deadzone": 0.2, "events": [] # <-- NO KEY BOUND! } jump={ "deadzone": 0.2, "events": [Space, Z] }
The mega_jump action has an empty events array — no key is assigned to trigger it. This is the "broken pair of legs" from the challenge hint: the powerful jump needed to reach the flag is intentionally broken.
world.gd — Loads the flag scene:
func _ready(): var f = load("uid://oehyajwxl0hy").instantiate() f.position = Vector2(400, -120) # Above viewport (y-down coords) f.scale = Vector2(0.5, 0.5) add_child(f)
The flag scene (f.tscn) is placed at y = -120, which is above the normal viewport in Godot's y-down coordinate system. The regular jump cannot reach this height, and the mega_jump that could reach it has no key binding.
f.tscn contains 56 ColorRect nodes (all dark red, Color(0.545, 0, 0, 1)) positioned to spell out text in blocky pixel-art letters. Key observations:
x=-19 to x=2996, y=-53 to y=250rotation properties (for diagonal strokes in letters like "A")scale = Vector2(-0.92, 1) (for mirroring the closing } brace from {)Rather than trying to patch the game to bind the mega_jump key, the flag can be extracted directly by rendering the ColorRect coordinates from f.tscn to an image.
#!/usr/bin/env python3 """Render f.tscn ColorRect nodes to reveal the hidden flag text""" from PIL import Image, ImageDraw import re import math # Parse f.tscn for ColorRect nodes scene_file = "recovered/f.tscn" with open(scene_file, 'r') as f: content = f.read() # Extract all ColorRect blocks blocks = content.split('[node name=') rects = [] for block in blocks: if 'ColorRect' not in block: continue left = float(re.search(r'offset_left\s*=\s*([-\d.]+)', block).group(1)) if re.search(r'offset_left', block) else 0 top = float(re.search(r'offset_top\s*=\s*([-\d.]+)', block).group(1)) if re.search(r'offset_top', block) else 0 right = float(re.search(r'offset_right\s*=\s*([-\d.]+)', block).group(1)) if re.search(r'offset_right', block) else 0 bottom = float(re.search(r'offset_bottom\s*=\s*([-\d.]+)', block).group(1)) if re.search(r'offset_bottom', block) else 0 rotation = 0 rot_match = re.search(r'rotation\s*=\s*([-\d.]+)', block) if rot_match: rotation = float(rot_match.group(1)) scale_x, scale_y = 1.0, 1.0 scale_match = re.search(r'scale\s*=\s*Vector2\(([-\d.]+),\s*([-\d.]+)\)', block) if scale_match: scale_x = float(scale_match.group(1)) scale_y = float(scale_match.group(2)) rects.append((left, top, right, bottom, rotation, scale_x, scale_y)) # Determine canvas bounds min_x = min(r[0] for r in rects) - 20 min_y = min(r[1] for r in rects) - 20 max_x = max(r[2] for r in rects) + 20 max_y = max(r[3] for r in rects) + 20 width = int(max_x - min_x) height = int(max_y - min_y) img = Image.new('RGB', (width, height), 'white') draw = ImageDraw.Draw(img) for left, top, right, bottom, rotation, sx, sy in rects: # Apply offset to canvas coordinates x1 = left - min_x y1 = top - min_y x2 = right - min_x y2 = bottom - min_y if rotation != 0: # For rotated rects, draw as polygon cx, cy = x1, y1 # Rotation origin is top-left in Godot w = x2 - x1 h = y2 - y1 corners = [(0, 0), (w, 0), (w, h), (0, h)] rotated = [] for px, py in corners: rx = px * math.cos(rotation) - py * math.sin(rotation) + cx ry = px * math.sin(rotation) + py * math.cos(rotation) + cy rotated.append((rx, ry)) draw.polygon(rotated, fill=(139, 0, 0)) else: draw.rectangle([x1, y1, x2, y2], fill=(139, 0, 0)) img.save('flag_render.png') print(f"Rendered {len(rects)} rectangles to flag_render.png") # Result: the image spells out tjctf{REDACTED}
The rendered image clearly shows the text tjctf{REDACTED} spelled out in dark red block letters.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar