$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: Navigate a 3D maze in an ELF binary with remote server interaction. Solution: Used DFS with backtracking to dynamically explore the unknown server-side maze, as the binary contained different test data.
"Within Vault 8707 are located master keys used to access any vault in the country. Unfortunately, the entrance was caved in long ago. There are decades old rumors that the few survivors managed to tunnel out deep underground and make their way to safety. Can you uncover their tunnel and break back into the vault?"
The challenge provided:
nc 83.136.248.107 38062Downloaded and extracted the challenge files. Found an ELF 64-bit binary called tunnel.
$ file tunnel tunnel: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked...
Found interesting strings that reveal the challenge mechanics:
$ strings tunnel | grep -E "(Direction|Cannot|flag|vault)" Direction (L/R/F/B/U/D/Q)? Cannot move that way /flag.txt HTB{fake_flag_for_testing} You break into the vault and read the secrets within...
Key findings:
Using objdump to identify key functions:
$ objdump -t tunnel | grep -E "(main|get_cell|prompt|flag)"
Key functions identified:
main — Main game loopget_cell — Calculate cell position in 3D mazeprompt_and_update_pos — Handle movement inputget_flag — Read and print flag on successFrom disassembly of get_cell:
// Pseudo-code reconstruction struct Cell { int x, y, z; // Coordinates int type; // 0=start, 1=path, 2=wall, 3=goal }; // Maze is 20x20x20 = 8000 cells // Each cell is 16 bytes (4 ints) Cell* get_cell(int x, int y, int z) { return &maze_data[z * 400 + y * 20 + x]; }
Maze parameters:
Direction mapping:
| Direction | Delta (x, y, z) |
|---|---|
| B (Back) | (-1, 0, 0) |
| F (Forward) | (+1, 0, 0) |
| L (Left) | (0, -1, 0) |
| R (Right) | (0, +1, 0) |
| D (Down) | (0, 0, -1) |
| U (Up) | (0, 0, +1) |
The main loop:
get_flag() and exitsInitially tried to extract the maze from the binary and solve it offline:
#!/usr/bin/env python3 """ Attempt 1: Extract maze from binary and solve with BFS """ from collections import deque # Maze data found at offset 0x20e0 in binary # Extracted all cells and built adjacency graph # BFS found path from (0,0,0) to (19,19,19) path = "UUUFRUFUFFRFFRRUURUFFURURRFRURUUUURRFFUUURUFRDRRURRFFFFFRFF"
Problem: When sending this path to the server, got "Cannot move that way" errors. The server has a DIFFERENT maze than the binary!
The binary contains a fake/test maze for local development. The actual challenge maze is generated server-side.
Since the maze is unknown, implemented DFS with backtracking to explore dynamically:
#!/usr/bin/env python3 """ TunnelMadness - Dynamic 3D Maze Solver Uses DFS with backtracking to explore unknown maze """ from pwn import * context.log_level = 'error' # Direction definitions directions = ['F', 'R', 'U', 'B', 'L', 'D'] reverse_dir = {'B': 'F', 'F': 'B', 'L': 'R', 'R': 'L', 'D': 'U', 'U': 'D'} dir_delta = { 'B': (-1, 0, 0), 'F': (1, 0, 0), 'L': (0, -1, 0), 'R': (0, 1, 0), 'D': (0, 0, -1), 'U': (0, 0, 1) } DIM = 20 # Maze dimension def solve_maze(): r = remote('83.136.248.107', 38062) visited = set() walls = set() current_pos = (0, 0, 0) visited.add(current_pos) path = [] # Stack of moves for backtracking def try_move(direction): """Attempt to move in given direction, return result""" r.recvuntil(b'?') r.sendline(direction.encode()) line = r.recvline() # Check for success (reached goal) if b'break into' in line.lower() or b'secrets' in line.lower(): print(f"[+] SUCCESS!") print(f"FLAG: {line.decode()}") rest = r.recvall(timeout=5) print(f"{rest.decode()}") return 'flag' # Check for wall collision if b'Cannot move' in line: return 'wall' return 'ok' move_count = 0 while True: moved = False # Try each direction (DFS exploration) for direction in directions: dx, dy, dz = dir_delta[direction] new_pos = (current_pos[0]+dx, current_pos[1]+dy, current_pos[2]+dz) # Skip if out of bounds if not all(0 <= c < DIM for c in new_pos): continue # Skip if already visited or known wall if new_pos in visited or new_pos in walls: continue # Try the move result = try_move(direction) move_count += 1 if result == 'flag': print(f"[+] Solved in {move_count} moves") print(f"[+] Path length: {len(path) + 1}") return if result == 'ok': # Move succeeded visited.add(new_pos) current_pos = new_pos path.append(direction) moved = True break else: # Hit a wall walls.add(new_pos) # If no valid move found, backtrack if not moved: if not path: print("[-] No solution found!") break # Backtrack: reverse last move last_move = path.pop() back_dir = reverse_dir[last_move] result = try_move(back_dir) move_count += 1 if result == 'flag': print(f"[+] Solved in {move_count} moves") return # Update position dx, dy, dz = dir_delta[back_dir] current_pos = (current_pos[0]+dx, current_pos[1]+dy, current_pos[2]+dz) if __name__ == '__main__': solve_maze()
$ python3 solve.py [+] SUCCESS! FLAG: You break into the vault and read the secrets within... HTB{REDACTED} [+] Solved in 118 moves
The DFS algorithm explored the 3D maze, backtracking when hitting dead ends, until reaching the goal cell at (19, 19, 19).
Many CTF challenges with a remote component contain test data in the binary for local debugging. The actual data is generated or stored on the server. Always verify whether local and server data match!
For dynamic maze exploration, DFS is ideal because:
6 directions in 3D:
This is a classic structure for 3D mazes, commonly found in CTF and game challenges.
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar