Inferno Sprint

Eh4x CTFby smothy

Inferno Sprint - EH4X CTF 2026

Category: Miscellaneous | Points: 500 | Solves: 0 (First Blood baby 🩸)


Challenge Description

The labyrinth burns. You have 90 seconds. Survive 5 rounds of increasingly brutal fire mazes. Dodge multi-speed fires, use portals, and escape to any edge — before the flames consume you.

nc 20.244.7.184 31337


First Look — "How bad can it be?"

So I connect to the server and...

nc 20.244.7.184 31337 ============================================================ INFERNO SPRINT | EHAX CTF ============================================================ Survive 5 rounds of burning mazes to claim the flag.

5 rounds. 90 seconds. Fire mazes. Portals.

Me reading the rules:

this is fine

No way I'm solving this by hand.

Yeah this is NOT a "solve by hand" challenge. 5 rounds, increasing grid sizes (15x15 up to 35x35), fire spreading everywhere, 90 second total timer. This screams scripted solver.


Understanding the Protocol

When you connect, the server dumps the rules then gives you rounds one by one. Each round looks like:

ROUND 1/5 SIZE 15 15 START 6 5 LIMIT 8 2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e 2e2e312e2e2e2e2e2e232e232e2e2e ... PATH>

Key things I noticed:

InfoMeaning
SIZE R CGrid dimensions (rows x cols)
START R CPlayer position (0-indexed)
LIMIT NMax moves allowed (near-optimal)
Hex rowsEach row is hex-encoded ASCII
PATH>Where you send your answer

The Grid Legend

After hex-decoding each row:

SymbolMeaning
MYou are here
.Empty cell (safe... for now)
#Wall (can't pass)
1Fire — spreads 1 cell EVERY turn (fast and angry)
2Fire — spreads 1 cell every 2 turns (medium)
3Fire — spreads 1 cell every 3 turns (slow but still scary)
a-ePortal pairs (step on one + send P = teleport to its twin)

Movement

Send one line of moves: W (up), S (down), A (left), D (right), P (portal).

Goal: reach ANY edge cell before fire catches you.


Hex Decoding — "Wait, 2e means what?"

Every grid row comes hex-encoded. Two hex chars = one ASCII character.

2e = '.' (0x2e = 46 = ASCII period) 23 = '#' (0x23 = 35 = ASCII hash) 4d = 'M' (0x4d = 77 = ASCII 'M') 31 = '1' (0x31 = 49 = ASCII '1') 32 = '2' 33 = '3' 61 = 'a' (portal) 62 = 'b' (portal) 63 = 'c' (portal)

So 2e2e312e2e decodes to ..1.. — two empty cells, a speed-1 fire, two more empty cells.

Quick python one-liner:

python
row = bytes.fromhex("2e2e312e2e").decode('ascii')
# Result: '..1..'

The Algorithm — BFS go brrrr

This is a classic time-expanded BFS (Breadth-First Search) problem. Here's the game plan:

BFS Algorithm Flow

Step 1: Fire Arrival Map

Before solving the player's path, we need to know when each cell catches fire.

Think of it like this — every fire source is a BFS root. Fire with speed K takes K turns to spread 1 cell. Multiple fires race each other, and the earliest arrival wins.

Fire speed 1: turn 0 → turn 1 → turn 2 (spreads every turn) Fire speed 2: turn 0 → turn 2 → turn 4 (spreads every 2 turns) Fire speed 3: turn 0 → turn 3 → turn 6 (spreads every 3 turns)

We run a multi-source BFS from ALL fire cells simultaneously:

python
def compute_fire_arrival(grid, rows, cols):
    fire_arrival = [[inf] * cols for _ in range(rows)]
    queue = deque()

    # Seed all fire sources
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] in '123':
                speed = int(grid[r][c])
                fire_arrival[r][c] = 0
                queue.append((r, c, 0, speed))

    while queue:
        r, c, turn, speed = queue.popleft()
        next_turn = turn + speed  # KEY: fire spreads 1 cell every 'speed' turns

        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r + dr, c + dc
            if in_bounds(nr, nc) and grid[nr][nc] != '#':
                if next_turn < fire_arrival[nr][nc]:
                    fire_arrival[nr][nc] = next_turn
                    queue.append((nr, nc, next_turn, speed))

    return fire_arrival

This gives us a 2D array where fire_arrival[r][c] = the earliest turn that cell is burning.

Visual example (Round 1 decoded grid):

............... Fire arrival times (partial): ............... ......##1...... The '1' at (2,8) spreads every turn: ........#...#.. turn 1: neighbors burn .........#.1... turn 2: next ring burns ............... etc. .....M......#.. ............... Player at (6,5) just needs to go ............... left 5 times: AAAAA → reaches col 0 (edge!) ........#.#.1.. .......#.1..... ..#...1..11.1.. ....1.......... ............... ...............

Step 2: Player BFS (Time-Aware)

Now we BFS from the player. The state is (row, col, turn).

A move is valid if:

  • The destination cell is not a wall
  • The destination cell is NOT on fire at turn + 1 (when we arrive)
  • We haven't exceeded the move limit
python
def solve_maze(grid, rows, cols, start_r, start_c, limit, fire_arrival, portals):
    queue = deque([(start_r, start_c, 0, "")])
    visited = {(start_r, start_c, 0)}

    while queue:
        r, c, turn, path = queue.popleft()
        if turn >= limit:
            continue

        for move, (dr, dc) in {'W':(-1,0),'S':(1,0),'A':(0,-1),'D':(0,1)}.items():
            nr, nc = r + dr, c + dc
            if valid(nr, nc) and grid[nr][nc] != '#' and fire_arrival[nr][nc] > turn + 1:
                if (nr, nc, turn+1) not in visited:
                    visited.add((nr, nc, turn+1))
                    new_path = path + move
                    if is_edge(nr, nc):
                        return new_path  # ESCAPED!
                    queue.append((nr, nc, turn+1, new_path))

        # Portal check
        if grid[r][c] in portals:
            # teleport to paired cell, costs 1 turn
            ...
    return None  # rip

Step 3: Portals

Portals are letter pairs (a-e). Standing on one and sending P teleports you to the other one (same letter). Costs 1 turn. This is just another "neighbor" in our BFS — instead of moving to an adjacent cell, you jump to the paired portal cell.

Portal 'a' at (9,14) <──────> Portal 'a' at (9,24) send 'P' to teleport between them

Watching It Solve — the satisfying part

--- Round 1 --- Size: 15x15 Path: AAAAA (5 moves) OK! --- Round 2 --- Size: 18x18 Path: WWWDWWW (7 moves) OK! --- Round 3 --- Size: 22x22 Path: WDDDDDDDDD (10 moves) OK! --- Round 4 --- Size: 28x28 Path: WWWWWWWWW (9 moves) OK! --- Round 5 --- Size: 35x35 Path: AWAAWAAAAAAAA (13 moves) OK! ============================================================ ALL ROUNDS CLEARED! FLAG: EH4X{1nf3rn0_spr1n7_bl4z3_runn3r_m4573r} ============================================================

First blood on a 500pt misc. Feels good.

celebration


Key Takeaways (Educational Bits)

1. BFS is your best friend for shortest path

BFS guarantees the shortest path in an unweighted graph. Since every move costs 1 turn, BFS finds the optimal escape route. If the graph had weighted edges, you'd use Dijkstra instead.

When to use what:

BFS → unweighted shortest path (this challenge) Dijkstra → weighted shortest path A* → weighted + heuristic (faster in practice) DFS → when you just need ANY path, not shortest

2. Time-expanded graphs

The trick here is that the graph changes over time (fire spreads). So instead of just tracking (row, col) as visited, we track (row, col, turn). This is called a time-expanded graph — basically treating each timestep as a separate "layer" of the graph.

Turn 0: [safe] [safe] [safe] Turn 1: [safe] [FIRE] [safe] Turn 2: [FIRE] [FIRE] [FIRE] ... fire keeps spreading

The player exists in this 3D space of (x, y, time).

3. Multi-source BFS for fire

Instead of running BFS from each fire source separately, we seed ALL fire sources into the queue at once with turn=0. This gives us the earliest arrival time at every cell in a single pass. Very efficient — O(rows * cols) total.

4. Precompute, then solve

The pattern here is super common in competitive programming / CTF:

  1. Precompute the "danger map" (fire arrival times)
  2. Query against it during path search

This separates concerns and keeps the solver clean.

5. Don't forget edge cases (literally)

The escape condition is reaching ANY edge cell — row 0, last row, col 0, or last col. Easy to forget one of these when coding in a hurry.


Full Solve Script

The complete solver is in solve.py — it handles all 5 rounds automatically over a socket connection. The core logic is ~100 lines of BFS.


Flag

EH4X{1nf3rn0_spr1n7_bl4z3_runn3r_m4573r}

translates to: inferno sprint blaze runner master

GG. The inferno didn't wait, but we were faster.


writeup by smothy | EH4X CTF 2026