← all writeups
WebHuntress CTF 2026

Search Mirror

A reflected-XSS box: /execute-4d56c5?q= drops your input straight into #preview via innerHTML, so <img src=x onerror=...> gives full JS execution. But the page itself holds no secret, and the callback endpoint refuses a plain request with browser_callback_required.

The "something to read" is a callback_nonce exposed at /api/clues — and it's only readable same-origin, which is why it has to come from the XSS rather than an outside request. So the chain is: read the nonce, call the callback with it, render the reflected flag back into the page.

<img src=x onerror="fetch('/api/clues').then(r=>r.json()).then(c=>fetch(`/callback-77e6ec?nonce=${c.callback_nonce}`)).then(r=>r.text()).then(t=>{document.getElementById('preview').insertAdjacentHTML('beforeend',`<pre>${t}</pre>`)})">

One thing that bit me on another instance: a literal + in a URL query string decodes to a space, so hand-writing '...'+c.nonce silently breaks the JS. Use template literals (no + anywhere) or encode + as %2B. A later timed variant was also one-shot and checked that the callback came from the feature page — so it genuinely had to fire through the reflected XSS, not a console call. Flag captured on every instance.