← Back to Writeups
HTBN/AReversing

TunnelMadness

XESXOR8/23/20267 min read
#reversing#htb#n/a

TunnelMadness

Platform: HackTheBox | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-29 | Status: Solved Techniques: binary_maze_extraction, dfs_backtracking, dynamic_maze_solving, server_interaction

Summary

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.

Recon

Port scan

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

Enumeration highlights

  • Event: hackthebox | ID: 20260129_hackthebox_tunnelmadness
  • Tags: maze, elf, 3d_maze, dynamic_analysis, dfs, backtracking
  • Indicators: 3D maze navigation, L/R/F/B/U/D directions, embedded test data differs from server, remote server interaction required
  • Source: 20260129_hackthebox_tunnelmadness.md

Foothold

Vulnerability / Misconfiguration

  1. Binary_maze_extraction
  2. Dfs_backtracking
  3. Dynamic_maze_solving
  4. Server_interaction
<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

  • binary_maze_extraction
  • dfs_backtracking
  • dynamic_maze_solving
  • server_interaction
  • Tags: maze, elf, 3d_maze, dynamic_analysis, dfs, backtracking

Original Writeup

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

Description

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

  • A downloadable binary file
  • A remote server: nc 83.136.248.107 38062

Analysis

Initial Reconnaissance

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

String Analysis

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:

  • Navigation prompt: "Direction (L/R/F/B/U/D/Q)?" — 6 directions for 3D movement plus Quit
  • Wall collision: "Cannot move that way" — indicates invalid moves
  • Flag path: "/flag.txt" — server reads flag from file
  • Test flag: "HTB{fake_flag_for_testing}" — embedded fake flag for local testing
  • Success message: "You break into the vault..." — indicates reaching the goal

Function Analysis

Using objdump to identify key functions:

$ objdump -t tunnel | grep -E "(main|get_cell|prompt|flag)"

Key functions identified:

  • main — Main game loop
  • get_cell — Calculate cell position in 3D maze
  • prompt_and_update_pos — Handle movement input
  • get_flag — Read and print flag on success

Understanding the Maze Structure

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

  • Dimensions: 20x20x20 (indices 0-19)
  • Cell size: 16 bytes (x, y, z, type)
  • Cell types: 0=start, 1=path, 2=wall, 3=goal
  • Start position: (0, 0, 0)
  • Goal position: (19, 19, 19)

Direction mapping:

DirectionDelta (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)

Main Loop Logic

The main loop:

  1. Prompts for direction input
  2. Calculates new position based on direction
  3. Checks if new cell is valid (not wall, within bounds)
  4. If cell type == 3 (goal), calls get_flag() and exits
  5. Otherwise, updates position and continues

Solution

First Attempt: Static Maze Extraction (Failed)

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

Second Attempt: Dynamic Maze Solving (Success)

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()

Execution

$ 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).

Notes

Why Static Analysis Failed

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!

DFS vs BFS Algorithm

  • BFS (Breadth-First Search) — finds the shortest path but requires knowledge of the entire graph in advance
  • DFS (Depth-First Search) with backtracking — works with an unknown graph, explores as it progresses

For dynamic maze exploration, DFS is ideal because:

  1. Does not require knowledge of the entire structure
  2. Backtracking allows returning from dead ends
  3. Memory O(depth) instead of O(all nodes)

3D Maze Navigation

6 directions in 3D:

  • Horizontal plane: Forward/Back (X), Left/Right (Y)
  • Vertical axis: Up/Down (Z)

This is a classic structure for 3D mazes, commonly found in CTF and game challenges.

</details>

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

signed by XESXOR