← Back to Writeups
HTBMediumMisc

Jumper

D3v0o0Nu119/2/20266 min read
#misc#htb#medium#gdscript_bytecode_decompilatio#godot_pck_extraction#input_action_mapping_analysis#tscn_scene_coordinate_renderin

Jumper

Platform: Tjctf 2026 | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-15 | Status: Solved Techniques: gdscript_bytecode_decompilation, godot_pck_extraction, input_action_mapping_analysis, tscn_scene_coordinate_rendering

Summary

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.

Recon

Port scan

bash
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: TJCTF 2026 | ID: 20260515_tjctf_jumper
  • Tags: game_hacking, wasm, godot, gdscript, pixel_art, pck_extraction, gdsdecomp, scene_analysis, platformer, input_mapping
  • Indicators: Godot game exported to WebAssembly (.wasm + .pck files), .pck file with GDPC magic bytes (Godot Pack format), GDSC bytecode with XOR 0xb6b6b6b6 encoded identifiers, Input action with empty events array (unbound key), Scene nodes positioned outside visible viewport (negative y coordinates)
  • Source: 20260515_tjctf_jumper.md

Foothold

Vulnerability / Misconfiguration

  1. gdscript_bytecode_decompilation
  2. godot_pck_extraction
  3. input_action_mapping_analysis
  4. tscn_scene_coordinate_rendering
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • gdscript_bytecode_decompilation
  • godot_pck_extraction
  • input_action_mapping_analysis
  • tscn_scene_coordinate_rendering
  • Tags: game_hacking, wasm, godot, gdscript, pixel_art, pck_extraction, gdsdecomp, scene_analysis, platformer, input_mapping

Original Writeup

<details><summary>Click to expand original content</summary>

Description

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.

Analysis

1. Reconnaissance

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 assets
  • jumper.wasm — WebAssembly binary (Godot runtime)
  • jumper.js — JavaScript glue code

2. PCK Extraction

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

3. Code Analysis

player.gd — Standard platformer controller (CharacterBody2D):

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

  • Regular jump (Space/Z): velocity.y = -370 — standard height
  • mega_jump: velocity.y -= 500 — much more powerful, only works on floor

project.godot — Input action mappings:

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

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

4. Flag Scene Analysis

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:

  • Coordinates range from x=-19 to x=2996, y=-53 to y=250
  • Some rectangles use rotation properties (for diagonal strokes in letters like "A")
  • Some use negative scale = Vector2(-0.92, 1) (for mirroring the closing } brace from {)

Solution

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.

python
#!/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.

</details>

signed by XESXOR