WebVerse - QuarterShift

WebVerse - QuarterShift

Lab: Quarter Shift
Difficulty: Hard
Theme: Multi-subdomain app + GraphQL + header-gated access tier + reset flow + SSRF into internal backoffice
Goal: Reach the internal backoffice and download the Jackpot Incident Report (flag inside)
Entry: http://quartershift.local


TL;DR

  1. Add quartershift.local to /etc/hosts and load the site.
  2. Vhost/subdomain fuzz to discover the main surfaces (portal, games, scores, auth, dashboard).
  3. Create an account on portal, log in, and land in games.
  4. Spot the GraphQL endpoint on the scores surface and run introspection.
  5. Grab /static/app.js, notice it references an exposed source map, and pull /static/app.js.map to recover a header hint.
  6. Use the discovered header to query moderator(id) successfully, then fuzz id until you retrieve a moderator reset code.
  7. Use the reset flow to set a new password for the moderator account and log in as moderator.
  8. Use the Dashboard fetch/export tooling as SSRF, fuzz internal subdomains through it, and find the internal backoffice.
  9. Use SSRF to fetch the jackpot incident report and extract the flag.

Setup: start with just the base domain

I keep the start minimal — one entry in /etc/hosts:

127.0.0.1 quartershift.local

Then browse:

http://quartershift.local

It looks like a marketing site with all the CTA's, which usually means there are other surfaces hiding behind subdomains. Instead of guessing, I fuzz.


Recon: vhost/subdomain fuzzing (Host header)

Since routing is based on the Host header, I can brute subdomains without DNS.

First I grab the baseline size of the default response:

curl -s http://quartershift.local/ | wc -c

Then I fuzz and filter out that baseline size:

ffuf -u 'http://quartershift.local/' -H 'Host: FUZZ.quartershift.local' -mc all --fs 5816 -w ~/tools/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt

That gives me a handful of surfaces that behave differently:

  • portal
  • games
  • scores
  • auth
  • dashboard

Now I add only what I’ve actually discovered to /etc/hosts:

127.0.0.1 quartershift.local
127.0.0.1 portal.quartershift.local
127.0.0.1 games.quartershift.local
127.0.0.1 scores.quartershift.local
127.0.0.1 auth.quartershift.local
127.0.0.1 dashboard.quartershift.local

Foothold: signup → games

I start by creating a normal player account on the portal:

http://portal.quartershift.local/signup

After registering and logging in, the app redirects me into the main casino frontend:

http://games.quartershift.local/

At this point I’m not trying to exploit anything yet — I’m mapping the web application flow. This is the “baseline recon” phase: watch redirects, cookies/JWTs, and most importantly, which APIs the frontend calls to load tournaments, leaderboards, and account data. From here, DevTools → Network becomes the main tool: every click is going to reveal an endpoint, an auth header, or a subdomain that matters later.


Quick win: check the frontend bundle for exposed hints (app.js.map)

Before I start hammering APIs, I do a quick “static recon” pass on what the frontend is shipping. In real web apps, exposed JavaScript bundles are full of accidental breadcrumbs — feature flags, internal route names, environment variables, and sometimes straight-up security assumptions.

From the Games surface, I pull the main JS bundle:

/static/app.js

I scroll near the top and immediately see the classic giveaway:

//# sourceMappingURL=app.js.map

That means a source map is publicly accessible, so I grab it too:

/static/app.js.map

Source maps often contain the original (unminified) code, comments, and developer notes — and in this case it pays off fast. Buried in the map is a reference to an access-tier header used for a kiosk-style client:

X-Client: kiosk

I don’t know exactly what it unlocks yet, but it’s a huge clue. A lot of production systems treat certain clients (mobile apps, kiosks, internal dashboards) as “trusted” and quietly gate extra fields or endpoints behind a simple header check. If Quarter Shift has a GraphQL API on the Scores subdomain, there’s a very real chance some queries or sensitive fields only respond when that kiosk header is present.


Find the GraphQL surface (Scores)

Back in the Games UI, I open DevTools → Network and refresh a couple pages (leaderboards, tournaments, anything that looks “live”). Almost immediately you can see the frontend acting like a thin client: it isn’t calculating standings locally — it’s pulling everything from a dedicated API surface.

All the tournament / leaderboard traffic is going to the Scores service:

http://scores.quartershift.local/graphql

That’s a big moment in the recon phase, because a GraphQL endpoint usually means:

  • one request can pull a lot of data if access controls are weak
  • the schema can reveal hidden objects and admin-only fields
  • “client-side” restrictions often collapse if the backend trusts the wrong things

So the next step is obvious: stop clicking around the UI and start interrogating the schema directly.


GraphQL introspection

With a GraphQL API, the fastest way to understand the attack surface is to ask the server what it exposes. Unless introspection is disabled, you can pull the schema and get a clean map of available queries, types, and fields — basically a blueprint of what the frontend (and sometimes internal tooling) can access.

For the payload style and common GraphQL recon patterns, I lean on PayloadsAllTheThings:

PayloadsAllTheThings/GraphQL Injection at master · swisskyrepo/PayloadsAllTheThings
A list of useful payloads and bypass for Web Application Security and Pentest/CTF - swisskyrepo/PayloadsAllTheThings

First I list the available query fields:

curl -s 'http://scores.quartershift.local/graphql' -X POST -H 'Content-Type: application/json'  -d '{"query":"query { __schema { queryType { fields { name } } } }"}' | jq .

One entry stands out immediately:

moderator(id: Int!): Moderator

That’s interesting because it implies there’s a first-class “moderator” object in the API — not just players and public scoreboard data.

Next, I inspect what the Moderator type actually contains:

curl -s 'http://scores.quartershift.local/graphql' -X POST -H 'Content-Type: application/json'  -d '{"query":"query { __type(name:\"Moderator\") { fields { name } } }"}' | jq .

This is where the whole lab direction snaps into focus. Among the normal fields, there’s one that should never be casually retrievable from an API:

resetCode

If that field is accessible for a real moderator record, it’s basically a direct line into an account takeover via the password reset flow. At that point the plan becomes: figure out what conditions make moderator(id) return data, then pull the reset code and pivot into moderator-only functionality.


Test the moderator(id) query + try the kiosk header

Now that I know the schema exposes moderator(id) (and that the Moderator type includes resetCode), I want to see how it behaves with a real request.

First I try the most basic query:

curl -s 'http://scores.quartershift.local/graphql' -X POST -H 'Content-Type: application/json' -d '{"query":"query { moderator(id: 1) { id email displayName resetCode } }"}' | jq .

The response comes back null.

That doesn’t automatically mean the query is useless — it usually means one of two things:

  • id: 1 simply isn’t a valid moderator record, or
  • the backend is enforcing some kind of access tier before returning moderator data

And earlier, the source map leak gave me a very specific hint: X-Client: kiosk. In real apps, this is a common pattern — “trusted client” headers that unlock additional fields or routes for kiosks, mobile apps, or internal dashboards.

So I rerun the exact same query, this time with the kiosk header:

curl -s 'http://scores.quartershift.local/graphql' -X POST -H 'Content-Type: application/json' -d '{"query":"query { moderator(id: 1) { id email displayName resetCode } }"}' -H 'X-Client: kiosk' | jq .

I still get null — but at this point, that’s not a dealbreaker. If the header is required, it won’t magically make an invalid ID start working. The real test is whether any ID returns a populated moderator object, and if the kiosk header is part of the conditions, I want it included from the start.

So the next step is simple: keep the kiosk header, fuzz the id, and look for the first response that isn’t null.


Fuzzing moderator IDs (with the kiosk header)

At this point I’m treating moderator(id) like any other ID-based lookup: the fastest way to find a valid record is to fuzz the ID space and filter out the “nothing here” response.

First I want a reliable filter. I send a request with a clearly invalid ID and measure the byte size of the response so I can exclude it during fuzzing:

curl -s 'http://scores.quartershift.local/graphql' -X POST -H 'Content-Type: application/json' -d '{"query":"query { moderator(id: 1) { id email displayName resetCode } }"}' -H 'X-Client: kiosk'  | wc -c

On my run, the null response is 29 bytes, which makes this easy to filter.

Next I generate a simple numeric wordlist:

seq 1 10000 > nums.txt

Now I fuzz the id parameter directly in the GraphQL query body, keeping the kiosk header in place and filtering out the 29-byte “null” responses:

ffuf -u 'http://scores.quartershift.local/graphql' -X POST -H 'Content-Type: application/json' -H 'X-Client: kiosk' -d '{"query":"query { moderator(id: FUZZ) { id email displayName resetCode } }"}' -w nums.txt -mc all --fs 29

After a bit of fuzzing, I get a hit — ID 21 returns a real moderator object instead of null.

Now I pull the full response cleanly for that ID:

curl -s 'http://scores.quartershift.local/graphql' -X POST -H 'Content-Type: application/json' -d '{"query":"query { moderator(id: 21) { id email displayName resetCode } }"}' -H 'X-Client: kiosk' | jq .

Moderator takeover: password reset → account takeover (ATO)

At this point the lab turns into a classic account takeover (ATO) chain. We’ve already done the hard part: we pulled a valid moderator record from GraphQL and leaked a password reset token / reset code (resetCode). In real-world terms, this is exactly what an attacker needs to hijack an account through the forgot password flow.

Now I just complete the reset workflow from the portal UI:

http://portal.quartershift.local/reset/confirm

On the reset confirmation page, I submit:

  • email: the moderator email returned by the GraphQL moderator(id) query
  • code: the leaked reset code / password reset token (resetCode)
  • new password: a password I control (this is the actual “takeover” moment)

Once the reset succeeds, I immediately authenticate as the moderator:

http://portal.quartershift.local/login

If the reset code is valid, this gives me a full moderator session — effectively privilege escalation via password reset, and the key pivot point that unlocks moderator-only surfaces like the dashboard, internal tooling, and anything tied to “trusted” roles.


Dashboard: find a server-side fetch primitive (SSRF)

After the account takeover, my goal is to find a server-side request feature — something that makes the backend fetch URLs on my behalf. In web security terms, this is where SSRF (Server-Side Request Forgery) usually appears: “URL preview”, “import from URL”, “fetch remote image”, “generate report from URL”, or any kind of admin tooling that accepts a URL and pulls content server-side.

I head to the moderator dashboard:

http://dashboard.quartershift.local/

From here I scan for anything that takes a URL input. Quarter Shift has a fetch tool that accepts a sourceUrl parameter:

/dashboard/tools/fetch?sourceUrl=...

To confirm it’s actually server-side (and not just the browser navigating), I point it at a normal, safe page and compare behavior. The response isn’t a redirect — the fetched HTML is returned inside the dashboard preview output. That means the server is making the outbound request and returning the result.

In other words: this is an SSRF primitive — a URL-fetch feature that can be abused for internal service access, internal subdomain discovery, and pivoting to private admin/backoffice interfaces that aren’t reachable directly from the outside.


Fuzz internal subdomains through SSRF (discover the backoffice)

Once I’ve confirmed the dashboard has an SSRF / URL fetch feature, the next move is to use it for internal host discovery. In real SSRF scenarios, the hardest part isn’t “making the request” — it’s figuring out what internal DNS names / subdomains / services actually exist behind the firewall.

I don’t have insider knowledge of any internal hostnames here, so I brute force them by placing the fuzz word directly into the hostname and letting the server request it via sourceUrl.

In Quarter Shift, the export endpoint is perfect for this because it will fetch the target URL server-side and return a response we can filter:

/exports/weekly?sourceUrl=...

First I learn what the “miss” looks like. When a subdomain doesn’t exist (or resolves to the default nothing-response), the response size is consistently 84 bytes, so I filter that out.

Then I fuzz the hostname position:

ffuf -u 'http://dashboard.quartershift.local/exports/weekly?sourceUrl=http://FUZZ.quartershift.local/' -H 'Cookie: qs_token=<YOUR_TOKEN>' -w ~/tools/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt --fs 84

Most of the results blend together — same response size, same generic output — which is exactly what you expect when you’re brute forcing internal names through SSRF.

But eventually, one hit stands out: the response size/content changes dramatically, and what comes back looks like a real admin/backoffice interface with navigation links and internal tooling. That’s the moment the SSRF pivot “lands”: I’ve gone from an external dashboard feature to a confirmed internal web app reachable only because the server is fetching it for me.


Once the SSRF subdomain fuzzing lands and I’m looking at a real backoffice page, I slow down and treat it like normal web app recon — just happening through an SSRF window instead of my browser talking to it directly.

The important point here is: I don’t need to guess endpoints or spray wordlists at internal paths. The backoffice HTML is already doing what internal admin panels always do — it exposes its own navigation.

So I read what the page gives me.

I load the backoffice home page through the SSRF export/fetch, then click through (or request) the obvious admin links it includes — things like Incidents, Payouts, Audit, etc. The “Incidents” section is the one that immediately fits the lab’s theme, so I follow that route first.

Inside the Incidents page, the jackpot entry includes a direct download link for the incident report. That becomes the final target: fetch the report URL through the same SSRF-enabled export endpoint and pull the file down locally.


Exfil: download the incident report

The dashboard export endpoint returns the fetched content as a file, so I point it at the jackpot report download URL (discovered from the backoffice page):

curl -s 'http://dashboard.quartershift.local/exports/weekly?sourceUrl=http://ops.quartershift.local/incidents/jackpot/download' -H 'Cookie: qs_token=<YOUR_TOKEN>'

Opening the report shows the incident writeup and the flag.


Wrap-up

Quarter Shift is a realistic, end-to-end web hacking attack chain where every step naturally unlocks the next — the same kind of compounding failures you see in real applications with multiple subdomains, internal tooling, and “trusted client” assumptions.

  • Subdomain discovery / vhost fuzzing maps the public-facing attack surface (portal, games, scores, auth, dashboard) and shows how much the application behavior changes based on the Host header. This is classic recon and enumeration for a multi-service web app.
  • An exposed JavaScript source map (app.js.map) leaks a trusted client header (X-Client: kiosk). That’s a common real-world mistake: developers rely on a header-based access tier to protect privileged functionality instead of enforcing proper server-side authorization.
  • GraphQL introspection turns the Scores API into a blueprint. Pulling the schema reveals hidden queries and sensitive fields — in this case a moderator(id) query and a resetCode field that becomes a direct account takeover (ATO) primitive.
  • Combining the kiosk header with ID fuzzing / BOLA-style ID enumeration lets you find a valid moderator record and extract the password reset code / reset token, leading straight into a password reset takeover and privileged session.
  • With moderator access, the dashboard exposes a URL fetch feature that becomes SSRF (Server-Side Request Forgery) — the key pivot. This is the classic “admin tool turns into internal network access” situation: URL previews, imports, exports, or fetch utilities that make server-side requests.
  • Using SSRF for internal subdomain discovery (fuzzing hostnames through sourceUrl) reveals an internal admin/backoffice panel that isn’t reachable directly. From there, the backoffice HTML provides the navigation to the incidents area and the jackpot incident report download route — no guessing required once you’re inside.

The big takeaway: this lab isn’t about one “magic bug.” It’s about how GraphQL schema exposure, trusted client header gating, BOLA/IDOR-style enumeration, password reset abuse, and SSRF internal pivoting chain together into a clean compromise of internal admin functionality.

If you want to run and solve Quarter Shift (and the rest of the WebVerse lab series) locally, you can grab the platform and labs at webverselabs.com.


FAQ: Quarter Shift (WebVerse) — GraphQL, Source Maps, Header Gating, and SSRF

What is the Quarter Shift WebVerse lab?

Quarter Shift is a hard WebVerse web and API security lab that combines multiple real-world vulnerability classes into one exploit chain, including subdomain enumeration, GraphQL introspection, header-gated access control bypass, password reset abuse, and SSRF into an internal backoffice application.

It is designed to feel like a realistic multi-service web app with separate surfaces (portal, games, scores, auth, dashboard) and internal tooling.

What vulnerabilities are demonstrated in Quarter Shift?

Quarter Shift demonstrates a chained attack path involving:

  • Host header / vhost subdomain enumeration
  • Exposed JavaScript source map (.map) information disclosure
  • Header-gated access tier logic (X-Client: kiosk)
  • GraphQL schema introspection
  • BOLA/IDOR-style ID enumeration on moderator(id)
  • Password reset token / reset code exposure
  • Account takeover (ATO) via reset flow abuse
  • SSRF (Server-Side Request Forgery) in dashboard fetch/export tooling
  • Internal subdomain discovery and pivoting to a backoffice app

The lab’s core lesson is how multiple “small” weaknesses compound into a full compromise.

Why is GraphQL introspection important in this lab?

GraphQL introspection is important because it turns the API into a self-documenting blueprint. In Quarter Shift, introspection reveals the existence of a moderator(id) query and the resetCode field on the Moderator type.

That gives you a direct path to test for sensitive field exposure, access-control issues, and account takeover opportunities.

Why is an exposed JavaScript source map (app.js.map) dangerous?

Exposed source maps are a form of information disclosure. They can reveal:

  • original (unminified) frontend code
  • internal route names
  • feature flags
  • comments / developer notes
  • hidden assumptions about client trust
  • headers used for access tiers

In Quarter Shift, the source map leaks the trusted client header hint (X-Client: kiosk), which becomes a key part of the GraphQL exploitation path.

What is the X-Client: kiosk header issue in Quarter Shift?

Quarter Shift demonstrates a common anti-pattern where the backend treats a client-supplied header (like X-Client: kiosk) as a trust signal for elevated access tiers.

This is dangerous because attackers can simply send that header manually. If sensitive functionality depends on header-based trust instead of proper authentication/authorization checks, it can lead to privilege escalation and sensitive data exposure.

Is the moderator ID fuzzing in Quarter Shift an IDOR or BOLA issue?

It behaves like a BOLA / IDOR-style enumeration pattern.

The moderator(id) GraphQL query allows ID-based lookups, and by fuzzing IDs you can identify valid moderator records. If sensitive fields (like resetCode) are exposed for those records, the API effectively becomes an account takeover primitive.

In API security terminology, this maps closely to Broken Object Level Authorization (BOLA) and sensitive field exposure.

How does the password reset flow lead to account takeover in Quarter Shift?

Once the GraphQL API leaks a valid moderator resetCode, the normal password reset confirmation flow can be abused to set a new password for that moderator account.

That creates a full Account Takeover (ATO) chain:

  1. Discover sensitive GraphQL query/fields
  2. Use header-gated access + ID fuzzing to retrieve a valid moderator record
  3. Extract the reset code
  4. Complete the reset flow
  5. Log in as moderator

This mirrors real-world reset flow abuse when reset tokens or reset codes are exposed.

What SSRF vulnerability is shown in Quarter Shift?

Quarter Shift includes a moderator dashboard feature that fetches URLs server-side using a sourceUrl parameter. This creates an SSRF (Server-Side Request Forgery) primitive because the server makes HTTP requests on behalf of the user and returns the response.

That SSRF primitive is then used for:

  • internal host discovery
  • internal subdomain fuzzing
  • accessing a private backoffice interface
  • downloading the jackpot incident report (flag)

Why is SSRF so dangerous in admin dashboards and export tools?

SSRF is especially dangerous in admin tools because “fetch”, “preview”, “import”, or “export from URL” features often run with server-side network access that normal users do not have.

If URL fetching is not restricted, attackers can use it to:

  • reach internal-only applications
  • enumerate internal DNS names / subdomains
  • access metadata services
  • pivot deeper into private infrastructure

Quarter Shift teaches this exact “admin tool → internal pivot” pattern.

What does Quarter Shift teach about exploit chaining in web security?

Quarter Shift teaches that modern web app compromises often come from chained weaknesses, not a single bug.

The attack path combines recon, info disclosure, GraphQL enumeration, access-control flaws, reset flow abuse, and SSRF pivoting. Each step unlocks the next, which is exactly how many real-world web and API attack chains work.

Can I practice Quarter Shift and similar labs online?

Yes — you can practice Quarter Shift and other WebVerse web and API security labs at webverselabs.com.

WebVerse is built for hands-on practice in areas like GraphQL security, API authorization flaws, SSRF, information disclosure, and multi-step exploit chains.

Read more