← Back to Writeups
CUSTOMEasyMisc

Лабиринт (Labyrinth)

XESXOR9/2/20265 min read
#misc#custom#easy#bfs_pathfinding#index_mapping_discovery#protocol_analysis

Лабиринт (Labyrinth)

Platform: duckerz | Category: Misc | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-01-09 | Status: Solved Techniques: bfs_pathfinding, index_mapping_discovery, protocol_analysis

Summary

Task: Navigate through 3 mazes (100x100, 200x200, 300x300) via netcat. Solution: BFS pathfinding with discovered wall index mapping bug (documented [N,E,S,W] vs actual [S,E,N,W]).

Лабиринт (Labyrinth) — duckerz CTF

Description

"Я гулял по парку, и задумавшись, не заметил как заблудился, помоги отыскать дорогу"

(Translation: "I was walking in the park, and lost in thought, I didn't notice how I got lost, help me find the way")

Connection: nc tasks.duckerz.ru 30003

Analysis

Service Interface

Upon connection, the service provides an interactive menu:

  1. Show labyrinth — visual representation of the maze
  2. Output wall matrix — [[[N, E, S, W], ...], ...] (documented format: up, right, down, left)
  3. Make a move — execute a move (N/E/S/W)
  4. Show completed count — number of completed mazes
  5. Show player position — player position [X, Y]
  6. Exit — exit ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Visual Elements

  • # (red) — exit point at position (0, 0)
  • @ (green) — current player position

Goal

Complete 3 mazes of increasing complexity:

  • Maze 1: 100×100
  • Maze 2: 200×200
  • Maze 3: 300×300

Key Discovery — Wall Index Bug

Server documentation claims the wall format is: [N, E, S, W]

However, testing revealed the actual format is: [S, E, N, W]!

IndexDocumentedActual
0NorthSouth
1EastEast
2SouthNorth
3WestWest
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Wall values: 0 = passage open, 1 = wall

This was discovered empirically — by testing each direction and comparing with the visual representation of the maze.

Solution

Algorithm

  1. Get the wall matrix from server (option 2)
  2. Get current player position (option 5)
  3. Use BFS to find path from current position to (0, 0)
  4. Execute the path by sending movement commands (option 3)
  5. Repeat for all 3 mazes

Solution Code

python
#!/usr/bin/env python3
"""
Duckerz CTF - Labyrinth Solver
Solves maze challenges using BFS pathfinding
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key insight: Server documentation says [N, E, S, W] but actual format is [S, E, N, W]
"""

import socket
import json
from collections import deque
import re

def connect():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(30)
    s.connect(('tasks.duckerz.ru', 30003))
    return s

def recv(s):
    """Receive data until prompt"""
    data = b''
    s.settimeout(5)
    try:
        while b'>> ' not in data:
            chunk = s.recv(4096)
            if not chunk:
                break
            data += chunk
    except:
        pass
    return data.decode('utf-8', errors='ignore')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def get_matrix(s):
    """Get wall matrix from server"""
    s.send(b'2\n')
    data = recv(s)
    for line in data.split('\n'):
        if line.startswith('[[['):
            return json.loads(line)
    return None

def get_position(s):
    """Get current player position"""
    s.send(b'5\n')
    data = recv(s)
    for line in data.split('\n'):
        if line.startswith('[') and ',' in line and not line.startswith('[[['):
            return json.loads(line)
    return None

def bfs_solve(matrix, start_x, start_y):
    """
    BFS pathfinding from (start_x, start_y) to (0, 0)
    
    CRITICAL: Wall indices are NOT as documented!
    Documentation says: [N, E, S, W]
    Actual format:      [S, E, N, W]
    
    Index mapping:
      0 = South (down, dy=+1)
      1 = East (right, dx=+1)
      2 = North (up, dy=-1)
      3 = West (left, dx=-1)
    """
    h, w = len(matrix), len(matrix[0])
    
    # Direction: (name, wall_index, dx, dy)
    directions = [
        ('N', 2, 0, -1),   # North: index 2, move up
        ('E', 1, 1, 0),    # East: index 1, move right
        ('S', 0, 0, 1),    # South: index 0, move down
        ('W', 3, -1, 0),   # West: index 3, move left
    ]
    
    queue = deque([(start_x, start_y, [])])
    visited = {(start_x, start_y)}
    
    while queue:
        cx, cy, path = queue.popleft()
        
        # Reached exit
        if cx == 0 and cy == 0:
            return path
        
        cell = matrix[cy][cx]
        
        for direction, wall_idx, dx, dy in directions:
            # Check if passage is open (0 = open, 1 = wall)
            if cell[wall_idx] == 0:
                nx, ny = cx + dx, cy + dy
                if 0 <= nx < w and 0 <= ny < h and (nx, ny) not in visited:
                    visited.add((nx, ny))
                    queue.append((nx, ny, path + [direction]))
    
    return None  # No path found
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def execute_path(s, path):
    """Execute movement path and check for flag"""
    for move in path:
        s.send(b'3\n')
        recv(s)
        s.send(f'{move}\n'.encode())
        response = recv(s)
        
        if 'DUCKERZ' in response:
            match = re.search(r'DUCKERZ\{[^}]+\}', response)
            if match:
                return match.group()
    return None

def solve_maze(s):
    """Solve single maze"""
    matrix = get_matrix(s)
    if not matrix:
        return None
    
    pos = get_position(s)
    if not pos:
        return None
    
    x, y = pos
    h, w = len(matrix), len(matrix[0])
    
    print(f"  Size: {w}x{h}, Start: ({x}, {y})")
    
    path = bfs_solve(matrix, x, y)
    if not path:
        print("  ERROR: No path found!")
        return None
    
    print(f"  Path length: {len(path)} steps")
    
    return execute_path(s, path)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def main():
    s = connect()
    recv(s)  # Initial prompt
    
    for i in range(5):  # Max attempts
        # Check progress
        s.send(b'4\n')
        data = recv(s)
        match = re.search(r'(\d)/(\d)', data)
        
        if match:
            current, total = match.groups()
            print(f"\nMaze {int(current)+1}: Progress {current}/{total}")
            
            if current == total:
                # All mazes completed, get flag
                s.send(b'1\n')
                response = recv(s)
                flag_match = re.search(r'DUCKERZ\{[^}]+\}', response)
                if flag_match:
                    print(f"\nFLAG: {flag_match.group()}")
                break
        
        flag = solve_maze(s)
        if flag:
            print(f"\nFLAG: {flag}")
            break
    
    s.close()
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

if __name__ == '__main__':
    main()

Execution Results

Maze 1: Progress 0/3
  Size: 100x100, Start: (14, 68)
  Path length: 226 steps

Maze 2: Progress 1/3
  Size: 200x200, Start: (28, 157)
  Path length: 499 steps

Maze 3: Progress 2/3
  Size: 300x300, Start: (60, 276)
  Path length: 814 steps

FLAG: DUCKERZ{REDACTED}

Lessons

  1. Always verify documentation — the documented format [N, E, S, W] was incorrect; actual format is [S, E, N, W]
  2. Test assumptions empirically — visual comparison with the matrix helped find the bug
  3. BFS is ideal for mazes — guarantees shortest path in unweighted graphs
  4. Automation is mandatory — sizes 100×100, 200×200, 300×300 are impossible to solve manually ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍