DEV Community

Yogeshwar Peela
Yogeshwar Peela

Posted on Originally published at exploitnotes.hashnode.dev

TryHackMe: Corridor - Writeup

Overview

Corridor is an easy TryHackMe box built around a single Flask/Werkzeug web app. The homepage presents an image of a corridor with thirteen clickable doors, each linking to a URL that is an MD5 hash of a number. Every visible door leads to an identical, empty placeholder page. The actual flag sits behind a door the app supports but never links to on the page - found only by reasoning about the pattern behind the visible URLs and testing the one number missing from it.

Recon

Standard nmap scan against the target:

nmap -A -Pn <MACHINE_IP> -o nmap
Enter fullscreen mode Exit fullscreen mode
PORT   STATE SERVICE VERSION
80/tcp open  http    Werkzeug httpd 2.0.3 (Python 3.10.2)
|_http-title: Corridor
|_http-server-header: Werkzeug/2.0.3 Python/3.10.2
Enter fullscreen mode Exit fullscreen mode

Only port 80 is open, running a Flask dev server (Werkzeug). The title "Corridor" matched the box name, so the web app was the entire attack surface.

Enumerating the Corridor

Pulled the homepage:

curl http://<MACHINE_IP>/
Enter fullscreen mode Exit fullscreen mode

The page is a single image (corridor.png) with an HTML <map> overlay containing thirteen clickable <area> elements. Each one links to a 32-character hex string:

<area ... href="c4ca4238a0b923820dcc509a6f75849b" ...>
<area ... href="c81e728d9d4c2f636f067f89cc14862c" ...>
<area ... href="eccbc87e4b5ce2fe28308fd9f2a7baf3" ...>
...
<area ... href="c51ce410c124a10e0db5e4b97fc2af39" ...>
Enter fullscreen mode Exit fullscreen mode

These are 32-character hex strings - the right length and format for MD5 hashes. Recognizing a few of them from memory (c4ca4238a0b923820dcc509a6f75849b is the well-known MD5 of "1"), it was clear each door's URL is simply md5(n) for n = 1 through 13, one per door on the corridor image.

Requested the first door directly:

curl http://<MACHINE_IP>/c4ca4238a0b923820dcc509a6f75849b -v
Enter fullscreen mode Exit fullscreen mode
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 632
Server: Werkzeug/2.0.3 Python/3.10.2
Enter fullscreen mode Exit fullscreen mode

Returned a generic "empty room" page:

<style>
    body{
        background-image: url("/static/img/empty_room.jpg");
        background-size:  cover;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Looped through all thirteen door hashes the same way:

for h in c4ca4238a0b923820dcc509a6f75849b c81e728d9d4c2f636f067f89cc14862c \
         eccbc87e4b5ce2fe28308fd9f2a7baf3 a87ff679a2f3e71d9181a67b7542122c \
         e4da3b7fbbce2345d7772b0674a318d5 1679091c5a880faf6fb5e6087eb1b2dc \
         8f14e45fceea167a5a36dedd4bea2543 c9f0f895fb98ab9159f51fd0297e236d \
         45c48cce2e2d7fbdea1afc51c7c6ad26 d3d9446802a44259755d38e6d163e820 \
         6512bd43d9caa6e02c990b0a82652dca c20ad4d76fe97759aa27a0c99bff6710 \
         c51ce410c124a10e0db5e4b97fc2af39; do
  echo "=== $h ==="
  curl -s http://<MACHINE_IP>/$h
  echo
done
Enter fullscreen mode Exit fullscreen mode

Every single one returned the identical empty-room page. All thirteen visible doors are dead ends by design - none of them hold the flag.

Checking for a Debug Console

With no result from the doors, checked whether the Werkzeug dev server exposed its interactive debugger:

curl -sI http://<MACHINE_IP>/
curl -s http://<MACHINE_IP>/console
Enter fullscreen mode Exit fullscreen mode
HTTP/1.0 200 OK
<title>404 Not Found</title>
<h1>Not Found</h1>
Enter fullscreen mode Exit fullscreen mode

Clean 404, no debug PIN prompt. Debug mode is off - that path was a dead end too.

Finding the Hidden Door

Since the app clearly derives each door's route from md5(n), and the corridor image only exposes doors 1 through 13, the natural next question was: does the app support n = 0, even though no door links to it?

Directory brute-forcing was also run in parallel to rule out unrelated hidden endpoints:

gobuster dir -u http://<MACHINE_IP>/ -w /usr/share/wordlists/dirb/common.txt -x php,txt,json
Enter fullscreen mode Exit fullscreen mode

This didn't turn up anything relevant before being stopped early - the real answer wasn't a generic wordlist guess, it was reasoning about the app's own hashing pattern. Computed md5("0"):

cfcd208495d565ef66e7dff9f98764da
Enter fullscreen mode Exit fullscreen mode

Requested it directly:

curl -s http://<MACHINE_IP>/cfcd208495d565ef66e7dff9f98764da
Enter fullscreen mode Exit fullscreen mode

Unlike every numbered door, this one rendered differently - a different background image and a flag banner:

<style>
    body{
        background-image: url("/static/img/empty_room.png");
        background-size:  cover;
    }
    h1 {
        width: 100%;
        position: absolute;
        top: 40%;
        text-align: center;
    }
</style>
<h1>
    flag{REDACTED}
</h1>
Enter fullscreen mode Exit fullscreen mode

Flag

flag{REDACTED}
Enter fullscreen mode Exit fullscreen mode

Takeaways

  • When a set of URLs all follow an obvious derivation pattern (here, md5(door_number) for doors 1-13), it's worth testing values outside the visible range before reaching for brute-force wordlists - the boundary case (0) was the intended answer, not a hidden path.

  • A generic dirb/gobuster wordlist won't find hash-derived routes; those only fall out of understanding the app's own logic, not guessing common path names.

  • Always check the Werkzeug/Flask dev server for an exposed debug console (/console) early - it's a free RCE path when present, and ruling it out quickly avoids wasted effort chasing it later.

  • Identical response bodies across every visible endpoint is itself a signal: it means the real content lives somewhere the UI doesn't expose, not that the challenge is broken.

Top comments (0)