Episode 01 · Kill chain

02 — Path Traversal

Download Lab

Get ep01-lab.zip from /downloads/ep01-lab.zip, unzip to a folder named ep01-lab, then follow Setup below. No Git repo required.

CVE-2023-20198 was a critical flaw in the web UI of Cisco IOS XE, allowing unauthenticated attackers on exposed devices to create privileged accounts and persist on the box—often followed by deep lateral movement across carrier cores.

This lab is not a Cisco emulator; it’s a focused Nginx exercise in the same spirit: how a “blocked” path can still be reached when normalization, aliases, and encoding don’t line up.

? Socratic question: What happens when you ask the server for /admin/../admin/ — is that the same resource as /admin/ before or after the server resolves ..?
Setup
  1. Download the lab zip using the Download Lab button above (same file as hackthenbuild.com/downloads/ep01-lab.zip).
  2. Unzip it. Your folder should be named ep01-lab and contain docker-compose.yml, nginx/, and html/.
  3. Open a terminal inside the ep01-lab folder.
  4. Run:
    docker-compose up

Leave this running. The lab listens on localhost:8080. If you use Docker Compose V2, run docker compose up instead.

TRY IT

Confirm the admin route is locked down for direct requests.

  1. Open a second terminal (keep Docker running).
  2. Request the admin page directly — you should see 403 Forbidden.
curl -i http://localhost:8080/admin/index.html
A 403 means Nginx matched location /admin and applied deny all. That’s the intended lock — but it isn’t the whole story.

EXPLOIT IT

URL encoding represents characters as a % plus two hex digits. For example, dot is %2e and slash is %2f, so a path segment ../ can appear on the wire as %2e%2e%2f—same traversal intent, different bytes.

This stack mounts a misaligned location /files + alias pair (classic Nginx footgun). A URI beginning with /files can still carry .. segments that collapse into /admin/ on disk after alias substitution — without ever matching the /admin location first.

  1. Run the traversal request below.
  2. You should get HTTP/1.1 200 and the body should include the fake admin banner.
curl -i http://localhost:8080/files../admin/index.html
Try an encoded variant once the raw traversal clicks: http://localhost:8080/files%2f%2e%2e%2f%2e%2e%2fadmin%2findex.html

FIX IT

In your ep01-lab folder, edit nginx/nginx.conf. Add a defense that rejects any URI containing .. (the commented template below was the missing control). Align location /files/ with a trailing slash and matching alias …/ if you keep this pattern.

Apply a change equivalent to:

--- nginx.conf (vulnerable)
+++ nginx.conf (hardened)
@@
         server {
             listen 80;
             ...
+        # Block traversal sequences anywhere in the URI
+        location ~ \.\. {
+            deny all;
+        }
+
         location /admin {
             deny all;
         }
 
-        location /files {
+        location /files/ {
             alias /usr/share/nginx/html/;
         }
-
-        # Missing defense ...
+        # (optional) keep explicit deny on /admin as defense-in-depth
From inside ep01-lab: docker-compose restart nginx (or docker compose restart nginx with Compose V2).

Re-run the exploit:

curl -i http://localhost:8080/files../admin/index.html

Expect 403 again — or 404 if the tightened /files/ prefix no longer matches the poisoned URI (both outcomes mean you’ve closed the hole).

Lab complete

You blocked traversal, restored least-privilege path handling, and verified with curl. Next step on the chain: trust boundaries around privileged portals — think CALEA and operator consoles under pressure.