NorthKorea

webxsscsp-bypassexfiltrationbots

A portal where operatives file mission reports and an automated reviewer endorses them. The flag isn’t in any database field you can reach — it exists inside the reviewer’s browser for exactly one moment, and the box is really about capturing that moment.

Spoiler-light notes — no flags, credentials or copy-paste exploits. Solve it yourself first.

Recon

One port, a reverse proxy in front of an application server. Registering an account exposes the actual workflow, and the workflow is the target:

  • Submit a report with a short identifier and a body of text.
  • A reviewer bot visits each submitted report and reads it.
  • The bot then fills in an endorsement field with its response — that response contains the flag — and clicks the endorse button.

That last point is the whole challenge. The flag is never written to anything we can read. It appears as text in the bot’s browser at the instant it populates that form, and then the bot navigates away. Meanwhile the bot’s session cookie is HttpOnly, so the classic steal-the-cookie move is off the table from the start.

The injection point

That identifier is reflected server-side inside a script block — the page interpolates it directly into an object literal in its own inline JavaScript, rather than into markup or into a quoted string.

The page ships a strict Content Security Policy, and the inline script block carries a nonce. It’s natural to assume that makes injection safe. It doesn’t, and understanding why is most of the lesson.

A CSP nonce protects the script block from being injected. It cannot protect code that is injected within a block the page already trusts. Because the input lands inside an object literal rather than in a string context, you can close the literal, run arbitrary statements, and then reopen a dummy literal so the rest of the original line still parses. The payload executes with the block’s trust — inline, nonce-blessed, and completely CSP-compliant.

Confirming it is easy: inject something that calls home and watch the reviewer’s request arrive.

Why the obvious approaches all fail

This is the interesting half of the box. Once you have script execution in the bot, the naive exfiltration options all die:

  • Reading document.cookie — empty. The session cookie is HttpOnly.
  • A timer — the bot navigates away before it fires.
  • Hooking the form’s submit — the endorsement form is rendered below the injected block, so at the moment our code runs, that form isn’t in the DOM yet.
  • A fetch() to a same-origin route — the flag isn’t served by any route; it only exists in the form the bot is about to fill.
  • An iframe — blocked outright by the frame-ancestors directive.

So the flag isn’t stored, isn’t in a cookie we can read, and isn’t behind a URL. The only place it exists is the outgoing form submission the bot is about to make.

The solution

Two observations unlock it:

  1. The form isn’t in the DOM now, but it will be parsed a moment later when the browser finishes rendering the rest of the page. So don’t look for it — listen for it.
  2. When the bot clicks the button, a submit event fires. Attach a listener on document in the capture phase (third argument true). Capture runs before the form’s own handler, so we read the form’s data at the moment it is fully populated and before navigation begins.

At that instant the form data contains the flag the bot just typed. Then it has to get off the machine — and the bot is about to navigate, which kills ordinary requests. The one API built for exactly this is navigator.sendBeacon(): it queues an asynchronous POST that the browser delivers even as the page unloads. It was designed for analytics on page exit; here it’s the only reliable channel.

So the payload does three things: register the capture-phase submit listener, serialise the form data, and beacon it out. Start a listener, submit the poisoned report, and wait for the bot to review it. The flag arrives in the beacon body.

Insights worth keeping

  • CSP + HttpOnly does not equal “XSS mitigated”. The nonce stopped us injecting a new script block and HttpOnly stopped cookie theft — but the bot’s actions (what it types, what it submits) remained fully readable from JavaScript. Exfiltration moved from exfiltrating secrets-at-rest to exfiltrating secrets-in-transit.
  • sendBeacon is the answer whenever the target navigates immediately. Any timer or fetch-based exfil loses the race; a beacon doesn’t.
  • Capture-phase listeners beat “the element doesn’t exist yet”. You rarely need the element to exist to observe an event that will bubble through an ancestor.

Takeaways

  • A CSP nonce secures the script tag, not the data inside it — never interpolate user input into a trusted script block, even “safely” inside a JS object.
  • HttpOnly blocks cookie theft, not script execution. Assume anything reachable from the DOM can be exfiltrated.
  • When the target is transient (a value typed into a form, a page that immediately navigates), observe events rather than polling state.
  • navigator.sendBeacon() survives unload — it’s the right tool for exfiltration against navigation.
  • Capture-phase event listeners let you observe a submit before the application’s own handler sees it.
  • If a secret is only ever in a browser’s memory, the exploit is to catch it in flight.