← Back to Writeups
HTBN/AMisc

parkour encoding

XESXOR8/23/20266 min read
#misc#htb#n/a

parkour encoding

Platform: Pingctf | Category: Misc | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-19 | Status: Solved Techniques: anvil_parser_block_extraction, ascii_left_shift_decode, binary_presence_encoding, palette_block_states_decoding

Summary

Task: Minecraft world save with diamond blocks arranged along X axis encoding binary data. Solution: extract block positions, build binary string from presence/absence, decode as ASCII shifted left by 1 bit (byte >> 1).

Recon

Port scan

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

Enumeration highlights

  • Event: pingctf | ID: 20260419_pingctf_parkour_encoding
  • Tags: minecraft, nbt, ascii_shift, mca_anvil, world_parsing, spatial_encoding, bitwise_encoding
  • Indicators: Minecraft world save with level.dat and region/*.mca files, blocks arranged in a single line along one axis (presence/absence = bit 1/0), decoded bytes all have MSB set (values > 0x7F) — try right-shift by 1, Version tag in level.dat shows Minecraft 1.21.x (DataVersion 4671)
  • Source: 20260419_pingctf_parkour_encoding.md

Foothold

Vulnerability / Misconfiguration

  1. Anvil_parser_block_extraction
  2. Ascii_left_shift_decode
  3. Binary_presence_encoding
  4. Palette_block_states_decoding
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

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

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • anvil_parser_block_extraction
  • ascii_left_shift_decode
  • binary_presence_encoding
  • palette_block_states_decoding
  • Tags: minecraft, nbt, ascii_shift, mca_anvil, world_parsing, spatial_encoding, bitwise_encoding

Original Writeup

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

Description

even the flag is parkour in parkour civilization

The challenge references the YouTube video "Parkour Civilization" by Oliver Age 24 — a Minecraft short film where parkour is the language. The provided file is a Minecraft 1.21.11 world save (~3.7 MB zip) containing region files with .mca anvil format.

Author: tomek7667 Flag format: ping{.*}

Analysis

Initial Recon

Unzipping the world save reveals a standard Minecraft Java Edition structure:

parkour encoding/
├── level.dat           # World metadata (NBT)
├── icon.png            # World icon
├── region/             # Overworld chunks (.mca files)
│   ├── r.-1.-1.mca
│   ├── r.-1.0.mca
│   ├── r.0.-1.mca
│   ├── r.0.0.mca
│   └── ... (20 files total)
├── DIM-1/              # Nether (empty)
├── DIM1/               # End (empty)
└── data/
    └── stopwatches.dat

The level.dat shows:

  • Version Name: 1.21.11
  • DataVersion: 4671
  • WorldGenSettings: flat world (superflat)

Block Extraction

Using the anvil-parser Python library to iterate all chunks and decode the palette-based block_states of each section:

Discovery: The world contains exactly:

  • 232 minecraft:diamond_block — all at Y=0, Z=0, with X ranging from 1 to 431
  • 1 minecraft:oak_wall_sign — at position (432, 2, 0)

The sign text reads:

congratz!
btw, did you get
the flag along
the way?

This confirms: the flag is encoded in the diamond block arrangement along the X axis.

Encoding Pattern

The diamonds form a parkour path — a single straight line where:

  • Diamond present at X → bit 1
  • Gap (no diamond) at X → bit 0

Building a binary string from X=1 to X=431 (length 431 bits):

1110000011010010110111001100111011110110110011001101100001101000...

The Core Trick: ASCII Shifted Left by 1

Initial decode as raw 8-bit ASCII produces bytes like:

  • 0xE0, 0xD2, 0xDC, 0xCE, 0xF6...

All bytes have MSB set (values > 0x7F) — this is the key indicator!

Observation:

  • 0xE0 >> 1 = 0x70 = 'p'
  • 0xD2 >> 1 = 0x69 = 'i'
  • 0xDC >> 1 = 0x6E = 'n'
  • 0xCE >> 1 = 0x67 = 'g'
  • 0xF6 >> 1 = 0x7B = '{'

The encoding is 7-bit ASCII placed in the top 7 bits of each byte (equivalent to ASCII × 2, or left-shifted by 1 bit). The lowest bit is always 0.

Solution

Step 1: Extract All Blocks from Minecraft World

#!/usr/bin/env python3
import anvil
import os
import math
import json

def decode_section(section):
    """Decode palette-based block_states from a chunk section."""
    bs = section['block_states']
    palette = [tag['Name'].value for tag in bs['palette'].tags]
    
    if 'data' not in bs or len(palette) == 1:
        # Single block type fills entire section
        return palette, None
    
    data = bs['data'].value
    bits = max(4, math.ceil(math.log2(len(palette))))
    mask = (1 << bits) - 1
    
    indices = []
    for long_val in data:
        u = long_val & ((1 << 64) - 1)
        for i in range(64 // bits):
            if len(indices) >= 4096:
                break
            indices.append((u >> (i * bits)) & mask)
        if len(indices) >= 4096:
            break
    
    return palette, indices

def extract_blocks(world_path):
    """Extract all non-air, non-bedrock blocks from world."""
    region_dir = os.path.join(world_path, 'region')
    blocks = []
    
    for fname in os.listdir(region_dir):
        if not fname.endswith('.mca'):
            continue
        
        # Parse region coordinates from filename r.X.Z.mca
        parts = fname.split('.')
        rx, rz = int(parts[1]), int(parts[2])
        
        region = anvil.Region.from_file(os.path.join(region_dir, fname))
        
        for cx in range(32):
            for cz in range(32):
                try:
                    chunk = region.chunk_data(cx, cz)
                except:
                    continue
                
                if chunk is None or 'sections' not in chunk:
                    continue
                
                chunk_x = rx * 32 + cx
                chunk_z = rz * 32 + cz
                
                for section in chunk['sections'].tags:
                    if 'block_states' not in section:
                        continue
                    
                    section_y = section['Y'].value
                    palette, indices = decode_section(section)
                    
                    if indices is None:
                        # Single block type
                        if palette[0] not in ('minecraft:air', 'minecraft:bedrock'):
                            for i in range(4096):
                                lx = i & 0xF
                                ly = (i >> 8) & 0xF
                                lz = (i >> 4) & 0xF
                                x = chunk_x * 16 + lx
                                y = section_y * 16 + ly
                                z = chunk_z * 16 + lz
                                blocks.append([palette[0], x, y, z])
                    else:
                        for i, idx in enumerate(indices):
                            block = palette[idx]
                            if block in ('minecraft:air', 'minecraft:bedrock'):
                                continue
                            lx = i & 0xF
                            ly = (i >> 8) & 0xF
                            lz = (i >> 4) & 0xF
                            x = chunk_x * 16 + lx
                            y = section_y * 16 + ly
                            z = chunk_z * 16 + lz
                            blocks.append([block, x, y, z])
    
    return blocks

# Extract blocks
blocks = extract_blocks('parkour encoding')
print(f"Total blocks: {len(blocks)}")

Step 2: Decode the Flag

# Filter diamond blocks
diamonds = [(x, y, z) for name, x, y, z in blocks 
            if name == 'minecraft:diamond_block']
print(f"Diamond blocks: {len(diamonds)}")  # 232

# Get X coordinates
diamonds_x = sorted([x for x, y, z in diamonds])
xset = set(diamonds_x)
xmin, xmax = min(diamonds_x), max(diamonds_x)
print(f"X range: {xmin} to {xmax}")  # 1 to 431

# Build binary string: 1 if diamond present, 0 if gap
bits = ''.join('1' if x in xset else '0' for x in range(xmin, xmax + 1))
print(f"Bit length: {len(bits)}")  # 431

# Pad to multiple of 8 (add trailing 0)
bits += '0'  # Now 432 bits = 54 bytes

# Decode with right-shift by 1 (the key trick!)
flag = ''.join(chr(int(bits[i:i+8], 2) >> 1) for i in range(0, len(bits), 8))
print(f"Flag: {flag}")

Output:

ping{REDACTED}
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR