Roadmap

844 Words · 3 Minutes, 50 Seconds

Challenge Name: rev_roadmap

Category: Reversing

Difficulty: Medium

Author: Quack

Challenge Description

In Brunnerne Inc.™, every request now flows through our new Edge Roadmap Router. Only stakeholders who follow the approved roadmap reach the internal dashboard.

We’ve attached the router configuration for full transparency (we’re very transparent now). Are you corpo enough to gain clearance?

The “router” is an nginx instance. We are given default.conf and an included roadmap-badges.conf. The whole access check is implemented purely with nginx map directives — no backend, no code. Reaching the location / “Cleared” page (HTTP 200) requires walking a path ($uri) that satisfies the chained maps. The winning path is the flag.

Approach

Step 1 — Understand the pieces

roadmap-badges.conf is a character → hex-byte substitution table (a “badge” per character):

"a" "7f"; "b" "59"; ... "r" "0a"; ... "{" "34"; "}" "3d";   (39 entries)

default.conf ties everything to the request path:

map $uri   $route          { default ""; "~^/(?<s>.*)$" $s; }   # route = path after '/'
map $route $route_len_ok   { default 0;  "~^.{41}$" 1; }        # route must be exactly 41 chars

Then 41 maps extract a single character at each position ($wp_XXXX = route[N]), and 41 more turn each extracted char into its badge byte via the substitution table ($badge_XXXX):

map $route $wp_95f3 { default ""; "~^.{0}(?<c>.)"  $c; }   # position 0
map $route $wp_6a2e { default ""; "~^.{1}(?<c>.)"  $c; }   # position 1  ... up to 40
map $wp_95f3 $badge_0f51 { include roadmap-badges.conf; }  # badge byte of route[0]

Step 2 — Recognise the state machine

Lines 91–131 are a chain of transition maps of the form:

map "${cp_IN}:${badge_B}" $cp_OUT { default "DETOUR"; "chk_STATE:HH" "chk_NEXT"; }

Read as a finite-state machine: “if the current state is chk_STATE and the badge byte at some position equals HH, move to state chk_NEXT.” Any mismatch yields "DETOUR", which poisons the rest of the chain. The variables thread the state through: $cp_OUT of one map is the ${cp_IN} of exactly one other.

Two special anchors:

map $cp_199f $reached_cleared { default 0; "CLEARED" 1; }
map "$reached_cleared$route_len_ok" $access { default 0; "11" 1; }

So $access == 1 requires both reaching CLEARED and a 41-char route.

Step 3 — Walk the chain and recover the path

The transitions form one linear thread of 41 steps (start + 40). Following it — each step fixes one (position, required byte) pair, and the byte is reversed through the badge table to a character — reconstructs the whole 41-character route:

import re
char2hex = {}                                   # badge table
for l in open("roadmap-badges.conf"):
    m = re.match(r'\s*"(.+?)"\s+"([0-9a-f]{2})"', l)
    if m: char2hex[m.group(1)] = m.group(2)
hex2char = {v: k for k, v in char2hex.items()}

conf = open("default.conf").read()
wp_pos   = {m[0]: int(m[1]) for m in re.findall(r'\$(wp_\w+) \{ default ""; "~\^\.\{(\d+)\}', conf)}
badge_wp = dict(re.findall(r'map \$(wp_\w+) \$(badge_\w+) \{ include', conf))
badge_pos = {b: wp_pos[w] for w, b in badge_wp.items()}

start, by_cin = None, {}
for line in conf.splitlines():
    m = re.match(r'.*map "\$\{(cp_\w+)\}:\$\{(badge_\w+)\}" \$(cp_\w+).*"(chk_\w+):([0-9a-f]{2})" "(chk_\w+|CLEARED)"', line)
    if m: by_cin[m[1]] = m.groups()          # cin, badge, cout, state, byte, next
    m = re.match(r'.*map \$(badge_\w+) \$(cp_\w+).*"([0-9a-f]{2})" "(chk_\w+)"', line)
    if m: start = m.groups()                 # badge, cout, byte, next

sol = {}
sol[badge_pos[start[0]]] = hex2char[start[2]]
cur_out, cur_state = start[1], start[3]
while True:
    cin, badge, cout, state, byte, nxt = by_cin[cur_out]
    sol[badge_pos[badge]] = hex2char[byte]
    if nxt == "CLEARED": break
    cur_out, cur_state = cout, nxt

print("".join(sol[i] for i in range(41)))

The walk fills all 41 positions with no conflicts and terminates at CLEARED.

Step 4 — Verify against the real router

Build the container and request the recovered path. Gotcha: curl treats { and } as URL-globbing metacharacters, so -g (globbing off) is required or the braces are mangled into a 403.

docker build -t roadmap:local .
docker run -d --name rm -p 3111:80 roadmap:local
curl -g "http://127.0.0.1:3111/brunner{c0rp0r4t3_r04dm4p_t0_ng1nx_h34rt}"
# -> HTTP 200: "Access cleared, stakeholder."

A one-character mutation of the path returns HTTP 403 (“Detour”), confirming the check is exact.

Flag

brunner{c0rp0r4t3_r04dm4p_t0_ng1nx_h34rt}

Reflections and Learnings


reversingnginxstatic-analysisfinite-state-machine

Reversing