From 46b305ad65e1f0e63000ccf60f5d42d1ddbe0747 Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 18:55:14 -0300 Subject: [PATCH 01/14] Create solveRot.py --- 2025/Ctrl-Panic/R0tnoT13/solveRot.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 2025/Ctrl-Panic/R0tnoT13/solveRot.py 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)}") From 15261c03c71ff90f79658346d37e971ecb7ffd16 Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 18:55:54 -0300 Subject: [PATCH 02/14] Create crypt.py --- 2025/Ctrl-Panic/R0tnoT13/crypt.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 2025/Ctrl-Panic/R0tnoT13/crypt.py 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()) From b67ee9ba57115b79f5b7be86d5038e410a29edca Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:02:40 -0300 Subject: [PATCH 03/14] Create README.md --- 2025/Ctrl-Panic/R0tnoT13/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 2025/Ctrl-Panic/R0tnoT13/README.md diff --git a/2025/Ctrl-Panic/R0tnoT13/README.md b/2025/Ctrl-Panic/R0tnoT13/README.md new file mode 100644 index 0000000..d250c31 --- /dev/null +++ b/2025/Ctrl-Panic/R0tnoT13/README.md @@ -0,0 +1,18 @@ +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 vi pero use 1 y 0 porque parece que es lo más normal) +un ciphertext cifrado usando el estado +El objetivo es reconstruir S y recuperar el mensaje cifrado. + +imagen1 + +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 crear un vector de 128 bits que representa S y agrega una ecuación por cada par (k, valor_filtrado) conocido. Además, incorpora 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 + +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 convierte el estado hexadecimal en bytes, hace el XOR byte a byte con el ciphertext y muestra el resultado. + +imagen3 From a5353e14c93b01fe4381048f6bfa2d787577d138 Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:07:50 -0300 Subject: [PATCH 04/14] Update and rename README.md to writeup.md --- 2025/Ctrl-Panic/R0tnoT13/README.md | 18 ---------- 2025/Ctrl-Panic/R0tnoT13/writeup.md | 52 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 18 deletions(-) delete mode 100644 2025/Ctrl-Panic/R0tnoT13/README.md create mode 100644 2025/Ctrl-Panic/R0tnoT13/writeup.md diff --git a/2025/Ctrl-Panic/R0tnoT13/README.md b/2025/Ctrl-Panic/R0tnoT13/README.md deleted file mode 100644 index d250c31..0000000 --- a/2025/Ctrl-Panic/R0tnoT13/README.md +++ /dev/null @@ -1,18 +0,0 @@ -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 vi pero use 1 y 0 porque parece que es lo más normal) -un ciphertext cifrado usando el estado -El objetivo es reconstruir S y recuperar el mensaje cifrado. - -imagen1 - -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 crear un vector de 128 bits que representa S y agrega una ecuación por cada par (k, valor_filtrado) conocido. Además, incorpora 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 - -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 convierte el estado hexadecimal en bytes, hace el XOR byte a byte con el ciphertext y muestra el resultado. - -imagen3 diff --git a/2025/Ctrl-Panic/R0tnoT13/writeup.md b/2025/Ctrl-Panic/R0tnoT13/writeup.md new file mode 100644 index 0000000..540b615 --- /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 vi pero usé `1` y `0` porque parece que es lo más normal) +- 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) From 20a111f872266776332b2c470d304265c5ed3d61 Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:09:07 -0300 Subject: [PATCH 05/14] Create solveDora.py --- 2025/Ctrl-Panic/Dor4_Null5/solveDora.py | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 2025/Ctrl-Panic/Dor4_Null5/solveDora.py 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()) From c94a2cbb84909e1d6ef7cfdae5085343693a730f Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:15:50 -0300 Subject: [PATCH 06/14] Create writeup.md --- 2025/Ctrl-Panic/Dor4_Null5/writeup.md | 135 ++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 2025/Ctrl-Panic/Dor4_Null5/writeup.md diff --git a/2025/Ctrl-Panic/Dor4_Null5/writeup.md b/2025/Ctrl-Panic/Dor4_Null5/writeup.md new file mode 100644 index 0000000..5869959 --- /dev/null +++ b/2025/Ctrl-Panic/Dor4_Null5/writeup.md @@ -0,0 +1,135 @@ +## 1. 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 + +--- + +## 2. 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 +``` + +--- + +## 3. 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) + From c020e187c6a207324d3a3d2ca655be8b617803b6 Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:16:39 -0300 Subject: [PATCH 07/14] Update writeup.md --- 2025/Ctrl-Panic/R0tnoT13/writeup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2025/Ctrl-Panic/R0tnoT13/writeup.md b/2025/Ctrl-Panic/R0tnoT13/writeup.md index 540b615..fa69b95 100644 --- a/2025/Ctrl-Panic/R0tnoT13/writeup.md +++ b/2025/Ctrl-Panic/R0tnoT13/writeup.md @@ -6,7 +6,7 @@ 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 vi pero usé `1` y `0` porque parece que es lo más normal) +- **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**. From bef7580678bea0f1c7f3a56cc311a39371d2b4c3 Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:17:36 -0300 Subject: [PATCH 08/14] Update writeup.md --- 2025/Ctrl-Panic/Dor4_Null5/writeup.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/2025/Ctrl-Panic/Dor4_Null5/writeup.md b/2025/Ctrl-Panic/Dor4_Null5/writeup.md index 5869959..4be7a4c 100644 --- a/2025/Ctrl-Panic/Dor4_Null5/writeup.md +++ b/2025/Ctrl-Panic/Dor4_Null5/writeup.md @@ -1,4 +1,4 @@ -## 1. Análisis del Problema +## 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**. @@ -31,7 +31,7 @@ Una función de verificación que utiliza un **XOR acumulativo** entre: --- -## 2. Identificación de Vulnerabilidades +## Identificación de Vulnerabilidades ### A. Hardcoded Secret (Insecure Storage) @@ -70,7 +70,7 @@ provided = expected ⊕ mask --- -## 3. Estrategia de Resolución (Exploit) +## Estrategia de Resolución (Exploit) Para resolver el reto se desarrolló un **script en Python** que realiza los siguientes pasos. From 74f7da9822a17a8ce5dcdd1d1b6b4e28504011c5 Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:23:30 -0300 Subject: [PATCH 09/14] Create writeup.md --- 2025/Ctrl-Panic/Mutation Mutation/writeup.md | 87 ++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 2025/Ctrl-Panic/Mutation Mutation/writeup.md 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) From 14a15529431abe551ff426bb1c95156f0e6abf06 Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:25:07 -0300 Subject: [PATCH 10/14] Create script.js --- 2025/Ctrl-Panic/Mutation Mutation/script.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 2025/Ctrl-Panic/Mutation Mutation/script.js 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){} +} From 45f27b847030b480f6310e32e163270cdc6f988b Mon Sep 17 00:00:00 2001 From: loladelloso Date: Mon, 16 Mar 2026 19:28:20 -0300 Subject: [PATCH 11/14] Create writeop.md --- .../lactf-invoice-generator/writeop.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 2025/Ctrl-Panic/lactf-invoice-generator/writeop.md diff --git a/2025/Ctrl-Panic/lactf-invoice-generator/writeop.md b/2025/Ctrl-Panic/lactf-invoice-generator/writeop.md new file mode 100644 index 0000000..ec6958e --- /dev/null +++ b/2025/Ctrl-Panic/lactf-invoice-generator/writeop.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 `