Introduction

Schrödinger's Scope

Difficulty:
Kevin in the Retro Store ponders pentest paradoxes—can you solve Schrödinger's Scope?

I’ll visit Kevin McFarland again in the Retro Emporium for another challenge involving the Schrodinger’s Scope:

image-20251123164447419
Kevin McFarland

Kevin McFarland

The Neighborhood College Course Registration System has been getting some updates lately and I’m wondering if you might help me improve its security by performing a small web application penetration test of the site.

For any web application test, one of the most important things for the test is the ‘scope’, that is, what one is permitted to test and what one should not. While hacking is fun and cool, professional integrity means respecting scope boundaries, especially when there are tempting targets outside our permitted scope.

Thankfully, the Neighborhood College has provided a very concise set of ‘Instructions’ which are accessible via a link provided on the site you will be testing. Do not overlook or dismiss the instructions! Following them is key to successfully completing the test.

Unfortunately, those pesky gnomes have found their way into the site and have been causing some mischief as well. Be wary of their presence and anything they may have to say as you are testing.

Can you help me demonstrate to the Neighborhood College that we know what responsible penetration testing looks like?

An eternal winter might sound poetic, but there’s a reason Tolkien’s heroes fought against endless darkness. A neighborhood frozen in time isn’t preservation—it’s stagnation. No spring astronomy observations, no summer shortwave propagation… just ice.

Clicking on the Schrodinger’s Scope terminal loads a website with instructions:

Instructions for the Registration System

Scope

Challenge

The main point of this challenge is to find vulnerabilities in the website without going out of scope, which is only down the path /register.

The /register/status_report page shows my successes and my violations:

image-20260101090200840

Just visiting / results in two violations. Once I hit too many violations, it says:

image-20260101090114550

There are little landmines hidden throughout the site that try to load stuff from places outside of scope. For example, on the main page, there’s a div that loads from /gnomeU:

<div class="mini-gnome-container"><img src="/gnomeU?id=39c71acd-2d2e-4fa6-8617-d618cce17269" id="mini-gnome" style="width: 20px; height: auto; position: fixed; left: 10px; bottom: 10px;"></div>

It’s this little image at the bottom right:

image-20260101090542114

Proxy

I played a bit with using Burp to block requests outside of scope, but I found that mitmproxy worked really nicely. I’ll write this script:

from mitmproxy import http
from mitmproxy import ctx


def request(flow: http.HTTPFlow) -> None:
    path = flow.request.path.split('?')[0]  # Remove query string for path check

    # Allow root path
    if path == "/":
        ctx.log.info(f"[IN SCOPE] {flow.request.method} {flow.request.path}")
        return

    # Allow anything under /register
    if path.startswith("/register"):
        ctx.log.info(f"[IN SCOPE] {flow.request.method} {flow.request.path}")
        return

    # Block everything else
    ctx.log.warn(f"[OUT OF SCOPE - BLOCKED] {flow.request.method} {flow.request.path}")
    flow.kill()

For each request coming through, it checks if the path is / or in /register, and if so, it logs it and returns (which allows the request continue). Otherwise, it prints a warning and kills the request.

With mitmproxy installed (uv tool install mitmproxy or pipx install mitmproxy), I’ll run it giving the script and a port:

oxdf@hacky$ mitmproxy -s scope_filter.py -p 8081

The default port is 8080, but I already have Burp listening there. In Burp, I’ll go to the settings and add an upstream proxy server:

image-20260101091808233

Now anything going through Burp will go through my proxy next. I’ll use FoxyProxy to send all traffic to this challenge to Burp.

https://...
Firefox FoxyProxy
Firefox + FoxyProxy
Burp
Burp
127.0.0.1:8080
Python
mitmproxy
127.0.0.1:8081
🎓
Server

If I load the main page with this setup, I’ll see logs in mitmproxy:

image-20260101092303736

Everything loads except the request to /gnomeU, which is killed. In Burp I see similar:

image-20260101092409371

Basic Site Enumeration

Site

The site root has a few links:

image-20251125172218673

Restart Session and Remove All Sessions will clear my progress. View Status Report show the vulnerabilities identified as well as the times I’ve been caught out of scope:

image-20251125172322839

Enter Registration System loads /register:

image-20251125172355984

On hovering over the gnome, they give a hint to check out the sitemap:

image-20251125172607225

It’s at /register/sitemap, which means it is in scope.

The login link leads to a login form:

image-20251125172432901

The gnome suggests checking out other login forms, and on hovering gives links to /auth/register/login and /admin:

image-20251125172512283

These are both out of scope, so I’ll ignore them.

Sitemap

I’ll grab the sitemap at /register/sitemap:

This XML file does not appear to have any style information associated with it. The document tree is shown below.
<urlset>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/admin
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/admin/console
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/admin/console/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/admin/logs
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/admin/logs/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/auth
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/auth/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/auth/register
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/auth/register/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/auth/register/login
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/auth/register/login/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/login
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/login/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/reset
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/reset/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/sitemap
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/sitemap/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/status_report
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/status_report/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/search
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/search/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/search/student_lookup
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/search/student_lookup/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/register
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/register/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/register/dev
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/register/dev/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/register/dev/dev_notes
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/register/dev/dev_notes/
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/register/dev/dev_todos
</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>
http://flask-schrodingers-scope-firestore.holidayhackchallenge.com/wip/register/dev/dev_todos/
</loc>
<changefreq>monthly</changefreq>
</url>
</urlset>

The paths summarize to:

📁 /admin
├── 📄 /console
└── 📄 /logs
📁 /auth
└── 📁 /register
└── 📄 /login
📁 /register in scope
├── 📄 /login
├── 📄 /reset
├── 📄 /sitemap
└── 📄 /status_report
📁 /search
└── 📄 /student_lookup
📁 /wip
└── 📁 /register
└── 📁 /dev
├── 📄 /dev_notes
└── 📄 /dev_todos

There’s five main paths, only one of which is in scope. All of the paths in /register are things I’ve already found.

Findings

Uncover developer information disclosure

I’ll note in the sitemap that wip (which likely stands for “work in progress”) has a /register path with a /dev path in it. It’s common for development sites to mirror paths on the production sites. It’s also common for sitemaps to be out of date or wrong or to hide certain endpoints.

It’s worth trying the dev paths in the given scope. /register/dev returns 404. But /register/dev/dev_notes returns 403:

image-20251125171708472

That implies it’s there, but blocking me, which is interesting. More interesting is /register/dev/dev_todos, which leaks valuable information including creds for teststudent, and provides the first finding:

image-20251125171633613

Uncovered developer information disclosure.

Exploit Information Disclosure via login

On the login form, I’ll try things like SQL injection and NoSQL injection, but not find anything that works. Now that I have creds, I’ll try those:

image-20251125172741138

I’ll turn on intercept in Burp Proxy and login again. It catches the request, and I’ll add an X-Forwarded-For header saying it came from 127.0.0.1. A common vulnerability in web applications is to use this header to validate the client’s location even though it’s fully under the control of the user.

image-20251125172910935

I’ll forward that on and disable intercept, and I’m logged into /register/courses:

image-20251125173053556

Exploited Information Disclosure via login

This page /register/courses just has a heading and no additional information. Looking at the raw HTML, there’s a block commented out:

image-20251125173905313

It shows where there will be a link to /register/courses/search “eventually”. I’ll edit the HTML in the browser dev tools so that it’s no longer commented:

image-20251126062156272

Now it shows:

image-20251125211631672

And that completes another task.

Found commented-out course search

Identify SQL injection vulnerability

The course search page has a form that takes a number:

image-20251125212240692

Searching for 1 shows all the courses with a 1 in them:

image-20251125212332492

The server is likely running something like:

select * from courses where id = 'HOL {user input}';

I’ll try a simple SQL injection payload ' or 1=1-- -, which would make the query:

select * from courses where id = 'HOL ' or 1=1-- -';

That should return all the courses, and it does return more:

image-20251125211900369

This completes another task.

Identified SQL injection vulnerability

Report the unauthorized gnome course

Clicking on the GNOME 827 course pops an alert:

image-20251125174448993

The correct answer is to “Report”. When you find an issue as a pentester, it’s not your job to fix it, but if it is important / serious, you must alert your POC immediately. Failure to do so in this case ends the assessment:

image-20251125212835430

Clicking report solves another issue:

image-20251125213033999

Visiting the page now shows:

image-20251125214248005

Reported the unauthorized gnome course

On the Search page the gnome says there’s another way:

image-20251125174751567

On hover:

image-20251125174758716

“page” leads out of scope to /dev/courseListings, but “notes” leads back to /register/dev/dev_notes, a page I got 403 on above but haven’t checked back on since authenticating. It says:

image-20251125174907532

/register/courses/holiday_behavior returns a 404, but /register/courses/wip/holiday_behavior returns a 403:

image-20251126061159988

The session registration value refers to a cookie that gets set on almost every HTTP response:

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: registration=eb72a05369dcb445; Path=/
X-Cloud-Trace-Context: 8a31409d34e55aad510944d7d98499c5;o=1
Date: Wed, 26 Nov 2025 10:57:04 GMT
Server: Google Frontend
Content-Length: 26663
Via: 1.1 google
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

It’s 16 hex characters, and if I look through my history, I’ll see it’s almost always the same first 14 characters. I’ll run a bash loop to collect a bunch of these cookies:

oxdf@hacky$ for i in {1..100}; do (curl https://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/courses/?id=12a560d4-532b-4ad0-b31b-23812b229d73 -b Schrodinger=25907106-bf36-4b7a-ad52-e7a8c1093450 -v -s 2>&1 | grep -i Set-Cookie | cut -d'=' -f2 | cut -d';' -f1) & done | sort | uniq -c | sort -nr
      8 eb72a05369dcb452
      7 eb72a05369dcb455
      7 eb72a05369dcb44e
      6 eb72a05369dcb456
      6 eb72a05369dcb44a
      6 eb72a05369dcb444
      6 eb72a05369dcb443
      5 eb72a05369dcb454
      5 eb72a05369dcb453
      5 eb72a05369dcb44f
      5 eb72a05369dcb448
      4 eb72a05369dcb451
      4 eb72a05369dcb44d
      4 eb72a05369dcb44b
      4 eb72a05369dcb447
      4 eb72a05369dcb446
      4 eb72a05369dcb445
      4 eb72a05369dcb442
      3 eb72a05369dcb450
      3 eb72a05369dcb449

The options seem to get set within a small range. Rather than get counts, I’ll sort and get only unique values:

oxdf@hacky$ for i in {1..100}; do (curl https://flask-schrodingers-scope-firestore.holidayhackchallenge.com/register/courses/?id=12a560d4-532b-4ad0-b31b-23812b229d73 -b Schrodinger=25907106-bf36-4b7a-ad52-e7a8c1093450 -v -s 2>&1 | grep -i Set-Cookie | cut -d'=' -f2 | cut -d';' -f1) & done | sort -u
eb72a05369dcb442
eb72a05369dcb443
eb72a05369dcb444
eb72a05369dcb445
eb72a05369dcb446
eb72a05369dcb447
eb72a05369dcb448
eb72a05369dcb449
eb72a05369dcb44a
eb72a05369dcb44b
eb72a05369dcb44d
eb72a05369dcb44e
eb72a05369dcb44f
eb72a05369dcb450
eb72a05369dcb451
eb72a05369dcb452
eb72a05369dcb453
eb72a05369dcb454
eb72a05369dcb455
eb72a05369dcb456

The last two characters range from 42-56 in hex, except 4c is missing! 0x4c is “L” in ASCII, so this is a classic “No L / Noel” joke. Someone else might already have that cookie. Setting my cookie to end in 4c allows the page to load:

image-20251126061941778

And solves the last finding.

✅ Hidden course found via cookie prediction

Outro

Schrödinger's Scope

Congratulations! You have completed the Schrödinger’s Scope challenge!

Now Loading any page gives a redirect to /register/all_vulnerabilities_found:

image-20251126062327225

Clicking “Finalize Test” loads a success page:

image-20251126062411011

Kevin is impressed:

Kevin McFarland

Kevin McFarland

Excellent work! You’ve just demonstrated one of the most valuable skills in cybersecurity - the ability to think like the original programmer and unravel their logic without needing to execute a single line of code.

Well done - you’ve shown the wisdom to stay within scope, uncover what mattered, and respecting other testing boundaries.

That kind of discipline is what separates a real penetration tester from someone just poking around.