Holiday Hack 2025: Schrödinger's Scope
Introduction
Schrödinger's Scope
Difficulty:❅❅❅❅❅I’ll visit Kevin McFarland again in the Retro Emporium for another challenge involving the Schrodinger’s Scope:
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:
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:
Just visiting / results in two violations. Once I hit too many violations, it says:
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:
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:
Now anything going through Burp will go through my proxy next. I’ll use FoxyProxy to send all traffic to this challenge to Burp.
If I load the main page with this setup, I’ll see logs in mitmproxy:
Everything loads except the request to /gnomeU, which is killed. In Burp I see similar:
Basic Site Enumeration
Site
The site root has a few links:
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:
Enter Registration System loads /register:
On hovering over the gnome, they give a hint to check out the sitemap:
It’s at /register/sitemap, which means it is in scope.
The login link leads to a login form:
The gnome suggests checking out other login forms, and on hovering gives links to /auth/register/login and /admin:
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:
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:
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:
✅ 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:
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.
I’ll forward that on and disable intercept, and I’m logged into /register/courses:
✅ Exploited Information Disclosure via login
Find commented-out course search
This page /register/courses just has a heading and no additional information. Looking at the raw HTML, there’s a block commented out:
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:
Now it shows:
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:
Searching for 1 shows all the courses with a 1 in them:
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:
This completes another task.
✅ Identified SQL injection vulnerability
Report the unauthorized gnome course
Clicking on the GNOME 827 course pops an alert:
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:
Clicking report solves another issue:
Visiting the page now shows:
✅ Reported the unauthorized gnome course
Hidden course found via cookie prediction
On the Search page the gnome says there’s another way:
On hover:
“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:
/register/courses/holiday_behavior returns a 404, but /register/courses/wip/holiday_behavior returns a 403:
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:
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:
Clicking “Finalize Test” loads a success page:
Kevin is impressed:
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.