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
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
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>/
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" ...>
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
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 632
Server: Werkzeug/2.0.3 Python/3.10.2
Returned a generic "empty room" page:
<style>
body{
background-image: url("/static/img/empty_room.jpg");
background-size: cover;
}
</style>
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
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
HTTP/1.0 200 OK
<title>404 Not Found</title>
<h1>Not Found</h1>
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
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
Requested it directly:
curl -s http://<MACHINE_IP>/cfcd208495d565ef66e7dff9f98764da
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>
Flag
flag{REDACTED}
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)