diff --git a/2025/Ctrl-Panic/Dor4_Null5/solveDora.py b/2025/Ctrl-Panic/Dor4_Null5/solveDora.py new file mode 100644 index 0000000..5ffc34b --- /dev/null +++ b/2025/Ctrl-Panic/Dor4_Null5/solveDora.py @@ -0,0 +1,54 @@ +from pwn import * +from Crypto.Hash import SHA256, HMAC +from Crypto.Protocol.KDF import HKDF +from Crypto.Cipher import AES + +def compute_path(navigation_key, challenge): + state = bytearray(16) + bytearray(challenge) + tracker = AES.new(navigation_key, AES.MODE_ECB) + for step in range(8): + scan = tracker.encrypt(bytes(state[step:step + 16])) + state[16 + step] ^= scan[0] + return bytes(state[-8:]) + +def solve_auth(secret_bytes, challenge_hex, server_token_hex): + challenge = bytes.fromhex(challenge_hex) + server_token = bytes.fromhex(server_token_hex) + + nav_key = HKDF( + master=secret_bytes, + key_len=16, + salt=challenge + server_token, + hashmod=SHA256 + ) + + expected = compute_path(nav_key, challenge) + h = HMAC.new(nav_key, expected, SHA256) + mask = h.digest()[:8] + + response = bytes([expected[i] ^ mask[i] for i in range(8)]) + return response.hex() + +HOST = 'dora-nulls.ctf.prgy.in' +PORT = 1337 + +# --- HIPÓTESIS NULL5 --- +# Intentamos con 64 bytes nulos (0x00) +SECRET = b"*" * 64 + +io = remote(HOST, PORT, ssl=True) +io.sendlineafter(b"choose ", b"1") + +my_challenge = "1122334455667788" +io.sendlineafter(b"challenge (hex): ", my_challenge.encode()) +io.sendlineafter(b"username: ", b"Administrator") + +io.recvuntil(b"server challenge: ") +server_token = io.recvline().strip().decode() +print(f"[*] Token: {server_token}") + +resp_hex = solve_auth(SECRET, my_challenge, server_token) +print(f"[*] Response: {resp_hex}") + +io.sendlineafter(b"response (hex): ", resp_hex.encode()) +print(io.recvall(timeout=2).decode()) diff --git a/2025/Ctrl-Panic/Dor4_Null5/writeup.md b/2025/Ctrl-Panic/Dor4_Null5/writeup.md new file mode 100644 index 0000000..4be7a4c --- /dev/null +++ b/2025/Ctrl-Panic/Dor4_Null5/writeup.md @@ -0,0 +1,135 @@ +## Análisis del Problema + +El desafío presentaba un **servicio de autenticación basado en un esquema de desafío-respuesta (Challenge-Response)**. +El objetivo era obtener acceso como el usuario **Administrator**. + +El código fuente revelaba tres componentes críticos: + +### Derivación de Clave (KDF) + +Se utiliza **HKDF** para generar una `navigation_key` a partir de: + +- un **secreto** +- un **challenge del cliente** +- un **token del servidor** + +### Generación de Ruta (`compute_path`) + +Un algoritmo que utiliza **AES en modo ECB** para transformar el challenge inicial en un valor de **8 bytes** llamado: + +``` +expected_path +``` + +### Verificación Vulnerable (`verify_credential`) + +Una función de verificación que utiliza un **XOR acumulativo** entre: + +- el valor esperado +- la respuesta del usuario +- una máscara HMAC + +--- + +## Identificación de Vulnerabilidades + +### A. Hardcoded Secret (Insecure Storage) + +En el diccionario `backpack`, el secreto del administrador estaba representado como: + +```python +"Administrator": "****************************************************************" +``` + +Aunque en muchos casos esto es un **placeholder**, el nombre del reto y la lógica sugerían que el secreto real eran literalmente **64 caracteres de asterisco**. + +Al probar este valor como `master_key` en **HKDF**, la derivación de la clave fue exitosa. + +--- + +### B. Manipulación Algorítmica (XOR Checksum) + +La vulnerabilidad técnica más profunda reside en cómo se valida la identidad. +El servidor **no compara hashes directamente**, sino que calcula un checksum: + +``` +checksum = expected ⊕ provided ⊕ mask +``` + +Para que la autenticación sea exitosa: + +``` +checksum = 0 +``` + +Esto permite a un atacante despejar el valor necesario de `provided` (la respuesta enviada al servidor): + +``` +provided = expected ⊕ mask +``` + +--- + +## Estrategia de Resolución (Exploit) + +Para resolver el reto se desarrolló un **script en Python** que realiza los siguientes pasos. + +### 1. Intercepción + +Conectarse al servidor y solicitar un inicio de sesión para `Administrator`, enviando un challenge arbitrario, por ejemplo: + +``` +1122334455667788 +``` + +--- + +### 2. Simulación Local + +Replicar localmente la lógica criptográfica del servidor. + +- Utilizar el `server_token` proporcionado por el servidor en tiempo real. +- Replicar la función **HKDF** usando como secreto: + +```python +b"*" * 64 +``` + +También se probaron previamente: + +```python +"0" * 64 +b"\x00" * 64 +``` + +porque el nombre del reto es **Dor4_Null5**. + +Luego: + +- Ejecutar la función `compute_path` (**AES-ECB**) para obtener el valor `expected`. + +--- + +### 3. Cálculo del HMAC + +Generar la máscara `mask` usando: + +- la `navigation_key` derivada +- el valor `expected` + +--- + +### 4. Inyección + +Calcular el XOR final entre `expected` y `mask` para generar la respuesta perfecta que **anula el checksum del servidor**: + +``` +provided = expected ⊕ mask +``` + +Esta respuesta se envía al servidor y permite autenticarse como **Administrator**. + +--- + +![imagen4](https://github.com/user-attachments/assets/f2b1f912-7a21-436f-aaed-d1493b402377) + diff --git a/2025/Ctrl-Panic/Mutation Mutation/script.js b/2025/Ctrl-Panic/Mutation Mutation/script.js new file mode 100644 index 0000000..82fad3d --- /dev/null +++ b/2025/Ctrl-Panic/Mutation Mutation/script.js @@ -0,0 +1,6 @@ +for(let i=0; i<500; i++){ + try{ + let res = _0x58c58c(i); + if(res.includes(‘lactf’)) console.log(“Encontrado en índice: “ + i + “: “ + res); + } catch(e){} +} diff --git a/2025/Ctrl-Panic/Mutation Mutation/writeup.md b/2025/Ctrl-Panic/Mutation Mutation/writeup.md new file mode 100644 index 0000000..b115f09 --- /dev/null +++ b/2025/Ctrl-Panic/Mutation Mutation/writeup.md @@ -0,0 +1,87 @@ +## Análisis del reto + +Al abrir el link se observa la siguiente página, donde no sucede nada al apretar **F12** o hacer **click derecho** para poder inspeccionar. + +![imagen5](https://github.com/user-attachments/assets/8cd589ff-837e-4538-ab01-140267aa8b2c) + +Para poder inspeccionar fue necesario hacer: + +``` +Tres puntos → Más herramientas → Herramientas para desarrolladores +``` + +--- + +## Análisis del código + +El reto presentaba un **script altamente ofuscado** que utilizaba técnicas de **antidebugging**. +El código incluía: + +### Diccionarios de strings + +Un **array de cadenas codificadas** utilizado para ocultar el flujo lógico del programa. + +### Intervalos y Timeouts + +Bucles infinitos que monitoreaban si: + +- la **consola estaba abierta** +- el **DOM cambiaba** + +En caso de detectar alguna de estas condiciones, el script **reseteaba el flag** a valores falsos como: + +``` +lactf{nope} +lactf{wrong_one} +``` + +--- + +## Congelar el script + +Para poder trabajar sin interferencias, el primer paso fue **"congelar" el estado del script** matando todos los procesos en segundo plano. + +Esto permitió evitar que los intervalos siguieran ejecutándose y nos devolvió **una flag falsa**. + +Al intentar ejecutar funciones obvias como: + +``` +f() +``` + +el script también devolvía **flags falsas**. + +Esto indicaba que: + +- el autor había implementado lógica para **detectar ejecución directa**, o +- el **flag real estaba fragmentado** dentro del código. + +![imagen6](https://github.com/user-attachments/assets/784b936c-e2cd-4f63-b315-4a9d80d48ec2) + +--- + +## Extracción del flag + +Se identificó la función de acceso al **diccionario principal de strings**: + +``` +_0x58c83c +``` + +En lugar de intentar entender la compleja matemática utilizada para calcular los índices, por ejemplo: + +``` +-0x3496d * -0x1 ... +``` + +se optó por una estrategia más directa: **extraer todos los strings almacenados en memoria**. + +Para ello se utilizó un **script de fuerza bruta** que recorría el diccionario y mostraba cualquier string que contuviera el prefijo: + +``` +lactf{ +``` + +Esto permitió localizar rápidamente los posibles candidatos al **flag real**. + +![imagen7](https://github.com/user-attachments/assets/2bdbfd3d-d332-4d83-a5f3-c0a75efa8e3f) diff --git a/2025/Ctrl-Panic/R0tnoT13/crypt.py b/2025/Ctrl-Panic/R0tnoT13/crypt.py new file mode 100644 index 0000000..41fd7c4 --- /dev/null +++ b/2025/Ctrl-Panic/R0tnoT13/crypt.py @@ -0,0 +1,8 @@ +from binascii import unhexlify +S = unhexlify("3721d4ef20940a4e78a4ab209a07acbd") +ct = unhexlify("477eb79b46ef667f16ddd94ca933c7c0") + +pt = bytes(a ^ b for a, b in zip(S, ct)) + +print(pt) +print(pt.decode()) diff --git a/2025/Ctrl-Panic/R0tnoT13/solveRot.py b/2025/Ctrl-Panic/R0tnoT13/solveRot.py new file mode 100644 index 0000000..5af8d74 --- /dev/null +++ b/2025/Ctrl-Panic/R0tnoT13/solveRot.py @@ -0,0 +1,38 @@ +from z3 import * +# Leaked values: k -> S xor ROTR(S, k) +leaks = { + 8: 183552667878302390742187834892988820241, + 4: 303499033263465715696839767032360064630, + 16: 206844958160238142919064580247611979450, + 2: 163378902990129536295589118329764595602, + 64: 105702179473185502572235663113526159091, + 32: 230156190944614555973250270591375837085, +} +solver = Solver() + +# 128-bit internal state +S = [Bool(f"S_{i}") for i in range(128)] + +# Anchor bits +solver.add(S[0] == True) +solver.add(S[127] == False) + +def get_bit(x, i): + return (x >> i) & 1 + +# Add equations +for k, value in leaks.items(): + for i in range(128): + bit = get_bit(value, i) + solver.add(Xor(S[i], S[(i + k) % 128]) == BoolVal(bit == 1)) + +assert solver.check() == sat +model = solver.model() + +# Rebuild S +state = 0 +for i in range(128): + if model[S[i]]: + state |= (1 << i) + +print(f"Recovered S: {hex(state)}") diff --git a/2025/Ctrl-Panic/R0tnoT13/writeup.md b/2025/Ctrl-Panic/R0tnoT13/writeup.md new file mode 100644 index 0000000..fa69b95 --- /dev/null +++ b/2025/Ctrl-Panic/R0tnoT13/writeup.md @@ -0,0 +1,52 @@ +El sistema mantiene un estado interno de **128 bits `S`**, derivado de AES. +Para verificar integridad de hardware, el firmware registra valores de la forma: +S ⊕ ROTR(S, k) +para distintos valores de rotación `k`. + +Por un error de logging, se filtran varios de estos valores, junto con: + +- los `k` correspondientes +- **dos bits conocidos del estado** (no los encontramos, pero usamos `1` y `0` porque es lo más normal en este tipo de retos) +- un **ciphertext cifrado usando el estado** + +El objetivo es **reconstruir `S` y recuperar el mensaje cifrado**. + +![imagen1](https://github.com/user-attachments/assets/7c7cd9fd-a717-4430-a7ef-36e9d1d9d899) + +--- + +## Reconstrucción del estado (`solveRot.py`) + +El script `solveRot.py` se encarga de **reconstruir el estado interno secreto `S` de 128 bits** usando la información filtrada por los logs del sistema. + +Cada log tiene la forma: +S ⊕ ROTR(S, k) +donde `ROTR` es una **rotación de bits hacia la derecha**. + +La idea clave es que estas ecuaciones son **lineales a nivel de bits**, así que pueden modelarse como **restricciones lógicas**. + +El script usa **Z3** para: + +1. Crear un vector de **128 bits** que representa `S`. +2. Agregar una ecuación por cada par `(k, valor_filtrado)` conocido. +3. Incorporar los **bits ancla del estado** (los bits conocidos que da el challenge) para romper simetrías y asegurar una solución única. + +Una vez que el solver encuentra un **modelo consistente**, el script reconstruye `S` completo y lo imprime en **hexadecimal**. + +![imagen2](https://github.com/user-attachments/assets/66f81e3e-f9d5-4061-8a84-ded1f5e0248f) + +--- + +## Descifrado del mensaje (`crypt.py`) + +Luego el script `crypt.py` toma el **estado interno `S` ya recuperado** y lo usa para descifrar el `ciphertext` provisto por el challenge. + +En este caso, el cifrado es simple: el estado actúa directamente como **keystream**, por lo que descifrar consiste en hacer un **XOR** entre el ciphertext y los bytes de `S`. + +El script: + +1. Convierte el **estado hexadecimal** en bytes. +2. Hace el **XOR byte a byte** con el ciphertext. +3. Muestra el **mensaje descifrado**. + +![imagen3](https://github.com/user-attachments/assets/d919b91d-f2df-460d-850e-10de6ff3009f) diff --git a/2025/Ctrl-Panic/README.md b/2025/Ctrl-Panic/README.md new file mode 100644 index 0000000..1eab505 --- /dev/null +++ b/2025/Ctrl-Panic/README.md @@ -0,0 +1,48 @@ +# CtrlPanic2026 – CTF Writeups + +## Integrantes + +- Dell´Oso Lola (03088/5) +- Soto Juliana (03157/1) +- Montagna Federica (02849/9) + +**Equipo:** CTRL+Panic +**Asignatura:** Desarrollo Seguro de Aplicaciones +**Universidad:** Universidad Nacional de La Plata – Facultad de Informática + +--- + +## Datos de los CTFs + +Los writeups de esta carpeta corresponden a distintos **Capture The Flag (CTF)** en los que participó el equipo **CTRL+Panic**. + +Durante estos eventos se resolvieron **4 retos** pertenecientes a **dos CTF diferentes**. + +--- + +### 🌐 LA CTF – Home | LA CTF (all skill levels) + +- **Fecha:** Vie 06, 23:00 — Dom 08, 23:00 (48 horas) +- **Plataforma:** https://platform.lac.tf + +En este CTF se resolvieron **dos retos de Web**. + +--- + +### 🔐 Pragyan CTF + +- **Fecha:** Vie 06, 10:00 — Dom 08, 10:00 (48 horas) +- **Categoría de los retos resueltos:** Cryptography + +En este evento se resolvieron **dos retos de criptografía**. + +--- + +## Desafíos resueltos + +| Challenge | CTF | Categoría | Vulnerabilidad / Técnica | +|---------|------|-----------|---------------------------| +| lactf-invoice-generator | LA CTF | Web | SSRF mediante renderizado de HTML en PDF (Puppeteer) | +| Mutation Mutation | LA CTF | Web / Reversing | JavaScript ofuscado y anti-debugging | +| R0tnoT13 | Pragyan CTF | Cryptography | Reconstrucción de estado usando ecuaciones XOR con rotaciones | +| Dor4_Null5 | Pragyan CTF | Cryptography | Bypass de autenticación por verificación basada en XOR | diff --git a/2025/Ctrl-Panic/lactf-invoice-generator/writeup.md b/2025/Ctrl-Panic/lactf-invoice-generator/writeup.md new file mode 100644 index 0000000..ec6958e --- /dev/null +++ b/2025/Ctrl-Panic/lactf-invoice-generator/writeup.md @@ -0,0 +1,110 @@ +## Análisis del reto + +El reto nos presenta una **aplicación web que genera facturas en formato PDF**. + +![imagen8](https://github.com/user-attachments/assets/6e2a83e5-4bdf-4656-aca2-cc7433441425) + +--- + +## Revisión del código + +Al revisar el código fuente proporcionado (`server.js`), observamos tres componentes importantes: + +### Generador de facturas (Express) + +El servidor recibe datos del usuario: + +- `name` +- `item` +- `cost` +- `datePurchased` + +Estos valores se **concatenan directamente dentro de una plantilla HTML**, sin ningún tipo de **sanitización o limpieza**. + +Esto significa que **el usuario puede inyectar HTML arbitrario** dentro de la factura. + +--- + +### Motor de generación de PDF (Puppeteer) + +La aplicación utiliza **Puppeteer**, que levanta un navegador **Chrome** en el servidor para: + +1. Renderizar el HTML generado +2. Convertirlo en un archivo **PDF** + +Como Puppeteer utiliza un **navegador real**, cualquier etiqueta HTML válida será interpretada. + +--- + +### Servicio interno de Flag + +Según el archivo `docker-compose.yml`, existe un servicio adicional: + +``` +flag +``` + +Este servicio corre en: + +``` +http://flag:8081 +``` + +y **no es accesible desde el exterior**, pero **sí es visible desde otros contenedores dentro de la red de Docker**, incluyendo el servidor que genera las facturas. + +![imagen9](https://github.com/user-attachments/assets/5af35535-830a-4b7e-a3a6-9902b4619ca0) + +--- + +## Identificación de la vulnerabilidad + +El punto débil se encuentra en la función: + +``` +generateInvoiceHTML +``` + +Debido a la ausencia de sanitización, es posible **inyectar etiquetas HTML arbitrarias**. + +Como Puppeteer renderiza el HTML en un navegador real, interpretará etiquetas como: + +``` + +``` + +Cuando Puppeteer renderiza la factura: + +1. El navegador del servidor carga el `